##// END OF EJS Templates
Added the ability to copy a project in the Project Administration panel....
Eric Davis -
r2608:fa7bd1c71dca
parent child
Show More
@@ -0,0 +1,16
1 <h2><%=l(:label_project_copy)%></h2>
2
3 <% labelled_tabular_form_for :project, @project, :url => { :action => "copy" } do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5
6 <fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
7 <% Redmine::AccessControl.available_project_modules.each do |m| %>
8 <label class="floating">
9 <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %>
10 <%= l_or_humanize(m, :prefix => "project_module_") %>
11 </label>
12 <% end %>
13 </fieldset>
14
15 <%= submit_tag l(:button_copy) %>
16 <% end %>
@@ -1,295 +1,319
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ProjectsController < ApplicationController
18 class ProjectsController < ApplicationController
19 menu_item :overview
19 menu_item :overview
20 menu_item :activity, :only => :activity
20 menu_item :activity, :only => :activity
21 menu_item :roadmap, :only => :roadmap
21 menu_item :roadmap, :only => :roadmap
22 menu_item :files, :only => [:list_files, :add_file]
22 menu_item :files, :only => [:list_files, :add_file]
23 menu_item :settings, :only => :settings
23 menu_item :settings, :only => :settings
24 menu_item :issues, :only => [:changelog]
24 menu_item :issues, :only => [:changelog]
25
25
26 before_filter :find_project, :except => [ :index, :list, :add, :activity ]
26 before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
27 before_filter :find_optional_project, :only => :activity
27 before_filter :find_optional_project, :only => :activity
28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
28 before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
29 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
29 before_filter :require_admin, :only => [ :add, :copy, :archive, :unarchive, :destroy ]
30 accept_key_auth :activity
30 accept_key_auth :activity
31
31
32 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
32 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
33 if controller.request.post?
33 if controller.request.post?
34 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
34 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
35 end
35 end
36 end
36 end
37
37
38 helper :sort
38 helper :sort
39 include SortHelper
39 include SortHelper
40 helper :custom_fields
40 helper :custom_fields
41 include CustomFieldsHelper
41 include CustomFieldsHelper
42 helper :issues
42 helper :issues
43 helper IssuesHelper
43 helper IssuesHelper
44 helper :queries
44 helper :queries
45 include QueriesHelper
45 include QueriesHelper
46 helper :repositories
46 helper :repositories
47 include RepositoriesHelper
47 include RepositoriesHelper
48 include ProjectsHelper
48 include ProjectsHelper
49
49
50 # Lists visible projects
50 # Lists visible projects
51 def index
51 def index
52 respond_to do |format|
52 respond_to do |format|
53 format.html {
53 format.html {
54 @projects = Project.visible.find(:all, :order => 'lft')
54 @projects = Project.visible.find(:all, :order => 'lft')
55 }
55 }
56 format.atom {
56 format.atom {
57 projects = Project.visible.find(:all, :order => 'created_on DESC',
57 projects = Project.visible.find(:all, :order => 'created_on DESC',
58 :limit => Setting.feeds_limit.to_i)
58 :limit => Setting.feeds_limit.to_i)
59 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
59 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
60 }
60 }
61 end
61 end
62 end
62 end
63
63
64 # Add a new project
64 # Add a new project
65 def add
65 def add
66 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
66 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
67 @trackers = Tracker.all
67 @trackers = Tracker.all
68 @project = Project.new(params[:project])
68 @project = Project.new(params[:project])
69 if request.get?
69 if request.get?
70 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
70 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
71 @project.trackers = Tracker.all
71 @project.trackers = Tracker.all
72 @project.is_public = Setting.default_projects_public?
72 @project.is_public = Setting.default_projects_public?
73 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
73 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
74 else
74 else
75 @project.enabled_module_names = params[:enabled_modules]
75 @project.enabled_module_names = params[:enabled_modules]
76 if @project.save
76 if @project.save
77 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
77 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
78 flash[:notice] = l(:notice_successful_create)
78 flash[:notice] = l(:notice_successful_create)
79 redirect_to :controller => 'admin', :action => 'projects'
79 redirect_to :controller => 'admin', :action => 'projects'
80 end
80 end
81 end
81 end
82 end
82 end
83
84 def copy
85 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
86 @trackers = Tracker.all
87 @root_projects = Project.find(:all,
88 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
89 :order => 'name')
90 if request.get?
91 @project = Project.copy_from(params[:id])
92 if @project
93 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
94 else
95 redirect_to :controller => 'admin', :action => 'projects'
96 end
97 else
98 @project = Project.new(params[:project])
99 @project.enabled_module_names = params[:enabled_modules]
100 if @project.copy(params[:id])
101 flash[:notice] = l(:notice_successful_create)
102 redirect_to :controller => 'admin', :action => 'projects'
103 end
104 end
105 end
106
83
107
84 # Show @project
108 # Show @project
85 def show
109 def show
86 if params[:jump]
110 if params[:jump]
87 # try to redirect to the requested menu item
111 # try to redirect to the requested menu item
88 redirect_to_project_menu_item(@project, params[:jump]) && return
112 redirect_to_project_menu_item(@project, params[:jump]) && return
89 end
113 end
90
114
91 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
115 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
92 @subprojects = @project.children.visible
116 @subprojects = @project.children.visible
93 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
117 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
94 @trackers = @project.rolled_up_trackers
118 @trackers = @project.rolled_up_trackers
95
119
96 cond = @project.project_condition(Setting.display_subprojects_issues?)
120 cond = @project.project_condition(Setting.display_subprojects_issues?)
97
121
98 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
122 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
99 :include => [:project, :status, :tracker],
123 :include => [:project, :status, :tracker],
100 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
124 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
101 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
125 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
102 :include => [:project, :status, :tracker],
126 :include => [:project, :status, :tracker],
103 :conditions => cond)
127 :conditions => cond)
104
128
105 TimeEntry.visible_by(User.current) do
129 TimeEntry.visible_by(User.current) do
106 @total_hours = TimeEntry.sum(:hours,
130 @total_hours = TimeEntry.sum(:hours,
107 :include => :project,
131 :include => :project,
108 :conditions => cond).to_f
132 :conditions => cond).to_f
109 end
133 end
110 @key = User.current.rss_key
134 @key = User.current.rss_key
111 end
135 end
112
136
113 def settings
137 def settings
114 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
138 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
115 @issue_category ||= IssueCategory.new
139 @issue_category ||= IssueCategory.new
116 @member ||= @project.members.new
140 @member ||= @project.members.new
117 @trackers = Tracker.all
141 @trackers = Tracker.all
118 @repository ||= @project.repository
142 @repository ||= @project.repository
119 @wiki ||= @project.wiki
143 @wiki ||= @project.wiki
120 end
144 end
121
145
122 # Edit @project
146 # Edit @project
123 def edit
147 def edit
124 if request.post?
148 if request.post?
125 @project.attributes = params[:project]
149 @project.attributes = params[:project]
126 if @project.save
150 if @project.save
127 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
151 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
128 flash[:notice] = l(:notice_successful_update)
152 flash[:notice] = l(:notice_successful_update)
129 redirect_to :action => 'settings', :id => @project
153 redirect_to :action => 'settings', :id => @project
130 else
154 else
131 settings
155 settings
132 render :action => 'settings'
156 render :action => 'settings'
133 end
157 end
134 end
158 end
135 end
159 end
136
160
137 def modules
161 def modules
138 @project.enabled_module_names = params[:enabled_modules]
162 @project.enabled_module_names = params[:enabled_modules]
139 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
163 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
140 end
164 end
141
165
142 def archive
166 def archive
143 @project.archive if request.post? && @project.active?
167 @project.archive if request.post? && @project.active?
144 redirect_to :controller => 'admin', :action => 'projects'
168 redirect_to :controller => 'admin', :action => 'projects'
145 end
169 end
146
170
147 def unarchive
171 def unarchive
148 @project.unarchive if request.post? && !@project.active?
172 @project.unarchive if request.post? && !@project.active?
149 redirect_to :controller => 'admin', :action => 'projects'
173 redirect_to :controller => 'admin', :action => 'projects'
150 end
174 end
151
175
152 # Delete @project
176 # Delete @project
153 def destroy
177 def destroy
154 @project_to_destroy = @project
178 @project_to_destroy = @project
155 if request.post? and params[:confirm]
179 if request.post? and params[:confirm]
156 @project_to_destroy.destroy
180 @project_to_destroy.destroy
157 redirect_to :controller => 'admin', :action => 'projects'
181 redirect_to :controller => 'admin', :action => 'projects'
158 end
182 end
159 # hide project in layout
183 # hide project in layout
160 @project = nil
184 @project = nil
161 end
185 end
162
186
163 # Add a new issue category to @project
187 # Add a new issue category to @project
164 def add_issue_category
188 def add_issue_category
165 @category = @project.issue_categories.build(params[:category])
189 @category = @project.issue_categories.build(params[:category])
166 if request.post? and @category.save
190 if request.post? and @category.save
167 respond_to do |format|
191 respond_to do |format|
168 format.html do
192 format.html do
169 flash[:notice] = l(:notice_successful_create)
193 flash[:notice] = l(:notice_successful_create)
170 redirect_to :action => 'settings', :tab => 'categories', :id => @project
194 redirect_to :action => 'settings', :tab => 'categories', :id => @project
171 end
195 end
172 format.js do
196 format.js do
173 # IE doesn't support the replace_html rjs method for select box options
197 # IE doesn't support the replace_html rjs method for select box options
174 render(:update) {|page| page.replace "issue_category_id",
198 render(:update) {|page| page.replace "issue_category_id",
175 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
199 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
176 }
200 }
177 end
201 end
178 end
202 end
179 end
203 end
180 end
204 end
181
205
182 # Add a new version to @project
206 # Add a new version to @project
183 def add_version
207 def add_version
184 @version = @project.versions.build(params[:version])
208 @version = @project.versions.build(params[:version])
185 if request.post? and @version.save
209 if request.post? and @version.save
186 flash[:notice] = l(:notice_successful_create)
210 flash[:notice] = l(:notice_successful_create)
187 redirect_to :action => 'settings', :tab => 'versions', :id => @project
211 redirect_to :action => 'settings', :tab => 'versions', :id => @project
188 end
212 end
189 end
213 end
190
214
191 def add_file
215 def add_file
192 if request.post?
216 if request.post?
193 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
217 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
194 attachments = attach_files(container, params[:attachments])
218 attachments = attach_files(container, params[:attachments])
195 if !attachments.empty? && Setting.notified_events.include?('file_added')
219 if !attachments.empty? && Setting.notified_events.include?('file_added')
196 Mailer.deliver_attachments_added(attachments)
220 Mailer.deliver_attachments_added(attachments)
197 end
221 end
198 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
222 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
199 return
223 return
200 end
224 end
201 @versions = @project.versions.sort
225 @versions = @project.versions.sort
202 end
226 end
203
227
204 def list_files
228 def list_files
205 sort_init 'filename', 'asc'
229 sort_init 'filename', 'asc'
206 sort_update 'filename' => "#{Attachment.table_name}.filename",
230 sort_update 'filename' => "#{Attachment.table_name}.filename",
207 'created_on' => "#{Attachment.table_name}.created_on",
231 'created_on' => "#{Attachment.table_name}.created_on",
208 'size' => "#{Attachment.table_name}.filesize",
232 'size' => "#{Attachment.table_name}.filesize",
209 'downloads' => "#{Attachment.table_name}.downloads"
233 'downloads' => "#{Attachment.table_name}.downloads"
210
234
211 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
235 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
212 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
236 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
213 render :layout => !request.xhr?
237 render :layout => !request.xhr?
214 end
238 end
215
239
216 # Show changelog for @project
240 # Show changelog for @project
217 def changelog
241 def changelog
218 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
242 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
219 retrieve_selected_tracker_ids(@trackers)
243 retrieve_selected_tracker_ids(@trackers)
220 @versions = @project.versions.sort
244 @versions = @project.versions.sort
221 end
245 end
222
246
223 def roadmap
247 def roadmap
224 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
248 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
225 retrieve_selected_tracker_ids(@trackers)
249 retrieve_selected_tracker_ids(@trackers)
226 @versions = @project.versions.sort
250 @versions = @project.versions.sort
227 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
251 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
228 end
252 end
229
253
230 def activity
254 def activity
231 @days = Setting.activity_days_default.to_i
255 @days = Setting.activity_days_default.to_i
232
256
233 if params[:from]
257 if params[:from]
234 begin; @date_to = params[:from].to_date + 1; rescue; end
258 begin; @date_to = params[:from].to_date + 1; rescue; end
235 end
259 end
236
260
237 @date_to ||= Date.today + 1
261 @date_to ||= Date.today + 1
238 @date_from = @date_to - @days
262 @date_from = @date_to - @days
239 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
263 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
240 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
264 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
241
265
242 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
266 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
243 :with_subprojects => @with_subprojects,
267 :with_subprojects => @with_subprojects,
244 :author => @author)
268 :author => @author)
245 @activity.scope_select {|t| !params["show_#{t}"].nil?}
269 @activity.scope_select {|t| !params["show_#{t}"].nil?}
246 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
270 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
247
271
248 events = @activity.events(@date_from, @date_to)
272 events = @activity.events(@date_from, @date_to)
249
273
250 respond_to do |format|
274 respond_to do |format|
251 format.html {
275 format.html {
252 @events_by_day = events.group_by(&:event_date)
276 @events_by_day = events.group_by(&:event_date)
253 render :layout => false if request.xhr?
277 render :layout => false if request.xhr?
254 }
278 }
255 format.atom {
279 format.atom {
256 title = l(:label_activity)
280 title = l(:label_activity)
257 if @author
281 if @author
258 title = @author.name
282 title = @author.name
259 elsif @activity.scope.size == 1
283 elsif @activity.scope.size == 1
260 title = l("label_#{@activity.scope.first.singularize}_plural")
284 title = l("label_#{@activity.scope.first.singularize}_plural")
261 end
285 end
262 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
286 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
263 }
287 }
264 end
288 end
265
289
266 rescue ActiveRecord::RecordNotFound
290 rescue ActiveRecord::RecordNotFound
267 render_404
291 render_404
268 end
292 end
269
293
270 private
294 private
271 # Find project of id params[:id]
295 # Find project of id params[:id]
272 # if not found, redirect to project list
296 # if not found, redirect to project list
273 # Used as a before_filter
297 # Used as a before_filter
274 def find_project
298 def find_project
275 @project = Project.find(params[:id])
299 @project = Project.find(params[:id])
276 rescue ActiveRecord::RecordNotFound
300 rescue ActiveRecord::RecordNotFound
277 render_404
301 render_404
278 end
302 end
279
303
280 def find_optional_project
304 def find_optional_project
281 return true unless params[:id]
305 return true unless params[:id]
282 @project = Project.find(params[:id])
306 @project = Project.find(params[:id])
283 authorize
307 authorize
284 rescue ActiveRecord::RecordNotFound
308 rescue ActiveRecord::RecordNotFound
285 render_404
309 render_404
286 end
310 end
287
311
288 def retrieve_selected_tracker_ids(selectable_trackers)
312 def retrieve_selected_tracker_ids(selectable_trackers)
289 if ids = params[:tracker_ids]
313 if ids = params[:tracker_ids]
290 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
314 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
291 else
315 else
292 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
316 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
293 end
317 end
294 end
318 end
295 end
319 end
@@ -1,337 +1,397
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 # Project statuses
19 # Project statuses
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
24 has_many :users, :through => :members
24 has_many :users, :through => :members
25 has_many :enabled_modules, :dependent => :delete_all
25 has_many :enabled_modules, :dependent => :delete_all
26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
27 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
27 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
28 has_many :issue_changes, :through => :issues, :source => :journals
28 has_many :issue_changes, :through => :issues, :source => :journals
29 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
29 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
30 has_many :time_entries, :dependent => :delete_all
30 has_many :time_entries, :dependent => :delete_all
31 has_many :queries, :dependent => :delete_all
31 has_many :queries, :dependent => :delete_all
32 has_many :documents, :dependent => :destroy
32 has_many :documents, :dependent => :destroy
33 has_many :news, :dependent => :delete_all, :include => :author
33 has_many :news, :dependent => :delete_all, :include => :author
34 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
34 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
35 has_many :boards, :dependent => :destroy, :order => "position ASC"
35 has_many :boards, :dependent => :destroy, :order => "position ASC"
36 has_one :repository, :dependent => :destroy
36 has_one :repository, :dependent => :destroy
37 has_many :changesets, :through => :repository
37 has_many :changesets, :through => :repository
38 has_one :wiki, :dependent => :destroy
38 has_one :wiki, :dependent => :destroy
39 # Custom field for the project issues
39 # Custom field for the project issues
40 has_and_belongs_to_many :issue_custom_fields,
40 has_and_belongs_to_many :issue_custom_fields,
41 :class_name => 'IssueCustomField',
41 :class_name => 'IssueCustomField',
42 :order => "#{CustomField.table_name}.position",
42 :order => "#{CustomField.table_name}.position",
43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
43 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
44 :association_foreign_key => 'custom_field_id'
44 :association_foreign_key => 'custom_field_id'
45
45
46 acts_as_nested_set :order => 'name', :dependent => :destroy
46 acts_as_nested_set :order => 'name', :dependent => :destroy
47 acts_as_attachable :view_permission => :view_files,
47 acts_as_attachable :view_permission => :view_files,
48 :delete_permission => :manage_files
48 :delete_permission => :manage_files
49
49
50 acts_as_customizable
50 acts_as_customizable
51 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
51 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
52 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
52 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
53 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
53 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
54 :author => nil
54 :author => nil
55
55
56 attr_protected :status, :enabled_module_names
56 attr_protected :status, :enabled_module_names
57
57
58 validates_presence_of :name, :identifier
58 validates_presence_of :name, :identifier
59 validates_uniqueness_of :name, :identifier
59 validates_uniqueness_of :name, :identifier
60 validates_associated :repository, :wiki
60 validates_associated :repository, :wiki
61 validates_length_of :name, :maximum => 30
61 validates_length_of :name, :maximum => 30
62 validates_length_of :homepage, :maximum => 255
62 validates_length_of :homepage, :maximum => 255
63 validates_length_of :identifier, :in => 1..20
63 validates_length_of :identifier, :in => 1..20
64 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
64 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
65
65
66 before_destroy :delete_all_members
66 before_destroy :delete_all_members
67
67
68 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] } }
68 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] } }
69 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
69 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
70 named_scope :public, { :conditions => { :is_public => true } }
70 named_scope :public, { :conditions => { :is_public => true } }
71 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
71 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
72
72
73 def identifier=(identifier)
73 def identifier=(identifier)
74 super unless identifier_frozen?
74 super unless identifier_frozen?
75 end
75 end
76
76
77 def identifier_frozen?
77 def identifier_frozen?
78 errors[:identifier].nil? && !(new_record? || identifier.blank?)
78 errors[:identifier].nil? && !(new_record? || identifier.blank?)
79 end
79 end
80
80
81 def issues_with_subprojects(include_subprojects=false)
81 def issues_with_subprojects(include_subprojects=false)
82 conditions = nil
82 conditions = nil
83 if include_subprojects
83 if include_subprojects
84 ids = [id] + descendants.collect(&:id)
84 ids = [id] + descendants.collect(&:id)
85 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
85 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
86 end
86 end
87 conditions ||= ["#{Project.table_name}.id = ?", id]
87 conditions ||= ["#{Project.table_name}.id = ?", id]
88 # Quick and dirty fix for Rails 2 compatibility
88 # Quick and dirty fix for Rails 2 compatibility
89 Issue.send(:with_scope, :find => { :conditions => conditions }) do
89 Issue.send(:with_scope, :find => { :conditions => conditions }) do
90 Version.send(:with_scope, :find => { :conditions => conditions }) do
90 Version.send(:with_scope, :find => { :conditions => conditions }) do
91 yield
91 yield
92 end
92 end
93 end
93 end
94 end
94 end
95
95
96 # returns latest created projects
96 # returns latest created projects
97 # non public projects will be returned only if user is a member of those
97 # non public projects will be returned only if user is a member of those
98 def self.latest(user=nil, count=5)
98 def self.latest(user=nil, count=5)
99 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
99 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
100 end
100 end
101
101
102 # Returns a SQL :conditions string used to find all active projects for the specified user.
102 # Returns a SQL :conditions string used to find all active projects for the specified user.
103 #
103 #
104 # Examples:
104 # Examples:
105 # Projects.visible_by(admin) => "projects.status = 1"
105 # Projects.visible_by(admin) => "projects.status = 1"
106 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
106 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
107 def self.visible_by(user=nil)
107 def self.visible_by(user=nil)
108 user ||= User.current
108 user ||= User.current
109 if user && user.admin?
109 if user && user.admin?
110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
111 elsif user && user.memberships.any?
111 elsif user && user.memberships.any?
112 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(',')}))"
112 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(',')}))"
113 else
113 else
114 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
114 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
115 end
115 end
116 end
116 end
117
117
118 def self.allowed_to_condition(user, permission, options={})
118 def self.allowed_to_condition(user, permission, options={})
119 statements = []
119 statements = []
120 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
120 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
121 if perm = Redmine::AccessControl.permission(permission)
121 if perm = Redmine::AccessControl.permission(permission)
122 unless perm.project_module.nil?
122 unless perm.project_module.nil?
123 # If the permission belongs to a project module, make sure the module is enabled
123 # If the permission belongs to a project module, make sure the module is enabled
124 base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)"
124 base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)"
125 end
125 end
126 end
126 end
127 if options[:project]
127 if options[:project]
128 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
128 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
129 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
129 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
130 base_statement = "(#{project_statement}) AND (#{base_statement})"
130 base_statement = "(#{project_statement}) AND (#{base_statement})"
131 end
131 end
132 if user.admin?
132 if user.admin?
133 # no restriction
133 # no restriction
134 else
134 else
135 statements << "1=0"
135 statements << "1=0"
136 if user.logged?
136 if user.logged?
137 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
137 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
138 allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
138 allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
139 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
139 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
140 elsif Role.anonymous.allowed_to?(permission)
140 elsif Role.anonymous.allowed_to?(permission)
141 # anonymous user allowed on public project
141 # anonymous user allowed on public project
142 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
142 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
143 else
143 else
144 # anonymous user is not authorized
144 # anonymous user is not authorized
145 end
145 end
146 end
146 end
147 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
147 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
148 end
148 end
149
149
150 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
150 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
151 #
151 #
152 # Examples:
152 # Examples:
153 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
153 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
154 # project.project_condition(false) => "projects.id = 1"
154 # project.project_condition(false) => "projects.id = 1"
155 def project_condition(with_subprojects)
155 def project_condition(with_subprojects)
156 cond = "#{Project.table_name}.id = #{id}"
156 cond = "#{Project.table_name}.id = #{id}"
157 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
157 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
158 cond
158 cond
159 end
159 end
160
160
161 def self.find(*args)
161 def self.find(*args)
162 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
162 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
163 project = find_by_identifier(*args)
163 project = find_by_identifier(*args)
164 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
164 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
165 project
165 project
166 else
166 else
167 super
167 super
168 end
168 end
169 end
169 end
170
170
171 def to_param
171 def to_param
172 # id is used for projects with a numeric identifier (compatibility)
172 # id is used for projects with a numeric identifier (compatibility)
173 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
173 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
174 end
174 end
175
175
176 def active?
176 def active?
177 self.status == STATUS_ACTIVE
177 self.status == STATUS_ACTIVE
178 end
178 end
179
179
180 # Archives the project and its descendants recursively
180 # Archives the project and its descendants recursively
181 def archive
181 def archive
182 # Archive subprojects if any
182 # Archive subprojects if any
183 children.each do |subproject|
183 children.each do |subproject|
184 subproject.archive
184 subproject.archive
185 end
185 end
186 update_attribute :status, STATUS_ARCHIVED
186 update_attribute :status, STATUS_ARCHIVED
187 end
187 end
188
188
189 # Unarchives the project
189 # Unarchives the project
190 # All its ancestors must be active
190 # All its ancestors must be active
191 def unarchive
191 def unarchive
192 return false if ancestors.detect {|a| !a.active?}
192 return false if ancestors.detect {|a| !a.active?}
193 update_attribute :status, STATUS_ACTIVE
193 update_attribute :status, STATUS_ACTIVE
194 end
194 end
195
195
196 # Returns an array of projects the project can be moved to
196 # Returns an array of projects the project can be moved to
197 def possible_parents
197 def possible_parents
198 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
198 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
199 end
199 end
200
200
201 # Sets the parent of the project
201 # Sets the parent of the project
202 # Argument can be either a Project, a String, a Fixnum or nil
202 # Argument can be either a Project, a String, a Fixnum or nil
203 def set_parent!(p)
203 def set_parent!(p)
204 unless p.nil? || p.is_a?(Project)
204 unless p.nil? || p.is_a?(Project)
205 if p.to_s.blank?
205 if p.to_s.blank?
206 p = nil
206 p = nil
207 else
207 else
208 p = Project.find_by_id(p)
208 p = Project.find_by_id(p)
209 return false unless p
209 return false unless p
210 end
210 end
211 end
211 end
212 if p == parent && !p.nil?
212 if p == parent && !p.nil?
213 # Nothing to do
213 # Nothing to do
214 true
214 true
215 elsif p.nil? || (p.active? && move_possible?(p))
215 elsif p.nil? || (p.active? && move_possible?(p))
216 # Insert the project so that target's children or root projects stay alphabetically sorted
216 # Insert the project so that target's children or root projects stay alphabetically sorted
217 sibs = (p.nil? ? self.class.roots : p.children)
217 sibs = (p.nil? ? self.class.roots : p.children)
218 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
218 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
219 if to_be_inserted_before
219 if to_be_inserted_before
220 move_to_left_of(to_be_inserted_before)
220 move_to_left_of(to_be_inserted_before)
221 elsif p.nil?
221 elsif p.nil?
222 if sibs.empty?
222 if sibs.empty?
223 # move_to_root adds the project in first (ie. left) position
223 # move_to_root adds the project in first (ie. left) position
224 move_to_root
224 move_to_root
225 else
225 else
226 move_to_right_of(sibs.last) unless self == sibs.last
226 move_to_right_of(sibs.last) unless self == sibs.last
227 end
227 end
228 else
228 else
229 # move_to_child_of adds the project in last (ie.right) position
229 # move_to_child_of adds the project in last (ie.right) position
230 move_to_child_of(p)
230 move_to_child_of(p)
231 end
231 end
232 true
232 true
233 else
233 else
234 # Can not move to the given target
234 # Can not move to the given target
235 false
235 false
236 end
236 end
237 end
237 end
238
238
239 # Returns an array of the trackers used by the project and its active sub projects
239 # Returns an array of the trackers used by the project and its active sub projects
240 def rolled_up_trackers
240 def rolled_up_trackers
241 @rolled_up_trackers ||=
241 @rolled_up_trackers ||=
242 Tracker.find(:all, :include => :projects,
242 Tracker.find(:all, :include => :projects,
243 :select => "DISTINCT #{Tracker.table_name}.*",
243 :select => "DISTINCT #{Tracker.table_name}.*",
244 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
244 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
245 :order => "#{Tracker.table_name}.position")
245 :order => "#{Tracker.table_name}.position")
246 end
246 end
247
247
248 # Deletes all project's members
248 # Deletes all project's members
249 def delete_all_members
249 def delete_all_members
250 Member.delete_all(['project_id = ?', id])
250 Member.delete_all(['project_id = ?', id])
251 end
251 end
252
252
253 # Users issues can be assigned to
253 # Users issues can be assigned to
254 def assignable_users
254 def assignable_users
255 members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
255 members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
256 end
256 end
257
257
258 # Returns the mail adresses of users that should be always notified on project events
258 # Returns the mail adresses of users that should be always notified on project events
259 def recipients
259 def recipients
260 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
260 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
261 end
261 end
262
262
263 # Returns an array of all custom fields enabled for project issues
263 # Returns an array of all custom fields enabled for project issues
264 # (explictly associated custom fields and custom fields enabled for all projects)
264 # (explictly associated custom fields and custom fields enabled for all projects)
265 def all_issue_custom_fields
265 def all_issue_custom_fields
266 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
266 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
267 end
267 end
268
268
269 def project
269 def project
270 self
270 self
271 end
271 end
272
272
273 def <=>(project)
273 def <=>(project)
274 name.downcase <=> project.name.downcase
274 name.downcase <=> project.name.downcase
275 end
275 end
276
276
277 def to_s
277 def to_s
278 name
278 name
279 end
279 end
280
280
281 # Returns a short description of the projects (first lines)
281 # Returns a short description of the projects (first lines)
282 def short_description(length = 255)
282 def short_description(length = 255)
283 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
283 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
284 end
284 end
285
285
286 # Return true if this project is allowed to do the specified action.
286 # Return true if this project is allowed to do the specified action.
287 # action can be:
287 # action can be:
288 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
288 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
289 # * a permission Symbol (eg. :edit_project)
289 # * a permission Symbol (eg. :edit_project)
290 def allows_to?(action)
290 def allows_to?(action)
291 if action.is_a? Hash
291 if action.is_a? Hash
292 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
292 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
293 else
293 else
294 allowed_permissions.include? action
294 allowed_permissions.include? action
295 end
295 end
296 end
296 end
297
297
298 def module_enabled?(module_name)
298 def module_enabled?(module_name)
299 module_name = module_name.to_s
299 module_name = module_name.to_s
300 enabled_modules.detect {|m| m.name == module_name}
300 enabled_modules.detect {|m| m.name == module_name}
301 end
301 end
302
302
303 def enabled_module_names=(module_names)
303 def enabled_module_names=(module_names)
304 if module_names && module_names.is_a?(Array)
304 if module_names && module_names.is_a?(Array)
305 module_names = module_names.collect(&:to_s)
305 module_names = module_names.collect(&:to_s)
306 # remove disabled modules
306 # remove disabled modules
307 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
307 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
308 # add new modules
308 # add new modules
309 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
309 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
310 else
310 else
311 enabled_modules.clear
311 enabled_modules.clear
312 end
312 end
313 end
313 end
314
314
315 # Returns an auto-generated project identifier based on the last identifier used
315 # Returns an auto-generated project identifier based on the last identifier used
316 def self.next_identifier
316 def self.next_identifier
317 p = Project.find(:first, :order => 'created_on DESC')
317 p = Project.find(:first, :order => 'created_on DESC')
318 p.nil? ? nil : p.identifier.to_s.succ
318 p.nil? ? nil : p.identifier.to_s.succ
319 end
319 end
320
320
321 # Copies and saves the Project instance based on the +project+.
322 # Will duplicate the source project's:
323 # * Issues
324 # * Members
325 # * Queries
326 def copy(project)
327 project = project.is_a?(Project) ? project : Project.find(project)
328
329 Project.transaction do
330 # Issues
331 project.issues.each do |issue|
332 new_issue = Issue.new
333 new_issue.copy_from(issue)
334 self.issues << new_issue
335 end
336
337 # Members
338 project.members.each do |member|
339 new_member = Member.new
340 new_member.attributes = member.attributes.dup.except("project_id")
341 new_member.project = self
342 self.members << new_member
343 end
344
345 # Queries
346 project.queries.each do |query|
347 new_query = Query.new
348 new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria")
349 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
350 new_query.project = self
351 self.queries << new_query
352 end
353
354 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
355 self.save
356 end
357 end
358
359
360 # Copies +project+ and returns the new instance. This will not save
361 # the copy
362 def self.copy_from(project)
363 begin
364 project = project.is_a?(Project) ? project : Project.find(project)
365 if project
366 # clear unique attributes
367 attributes = project.attributes.dup.except('name', 'identifier', 'id', 'status')
368 copy = Project.new(attributes)
369 copy.enabled_modules = project.enabled_modules
370 copy.trackers = project.trackers
371 copy.custom_values = project.custom_values.collect {|v| v.clone}
372 return copy
373 else
374 return nil
375 end
376 rescue ActiveRecord::RecordNotFound
377 return nil
378 end
379 end
380
321 protected
381 protected
322 def validate
382 def validate
323 errors.add(:identifier, :invalid) if !identifier.blank? && identifier.match(/^\d*$/)
383 errors.add(:identifier, :invalid) if !identifier.blank? && identifier.match(/^\d*$/)
324 end
384 end
325
385
326 private
386 private
327 def allowed_permissions
387 def allowed_permissions
328 @allowed_permissions ||= begin
388 @allowed_permissions ||= begin
329 module_names = enabled_modules.collect {|m| m.name}
389 module_names = enabled_modules.collect {|m| m.name}
330 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
390 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
331 end
391 end
332 end
392 end
333
393
334 def allowed_actions
394 def allowed_actions
335 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
395 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
336 end
396 end
337 end
397 end
@@ -1,48 +1,52
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add' %>
2 <%= link_to l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add' %>
3 </div>
3 </div>
4
4
5 <h2><%=l(:label_project_plural)%></h2>
5 <h2><%=l(:label_project_plural)%></h2>
6
6
7 <% form_tag({}, :method => :get) do %>
7 <% form_tag({}, :method => :get) do %>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
9 <label><%= l(:field_status) %> :</label>
9 <label><%= l(:field_status) %> :</label>
10 <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
10 <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
11 <label><%= l(:label_project) %>:</label>
11 <label><%= l(:label_project) %>:</label>
12 <%= text_field_tag 'name', params[:name], :size => 30 %>
12 <%= text_field_tag 'name', params[:name], :size => 30 %>
13 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
13 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
14 </fieldset>
14 </fieldset>
15 <% end %>
15 <% end %>
16 &nbsp;
16 &nbsp;
17
17
18 <table class="list">
18 <table class="list">
19 <thead><tr>
19 <thead><tr>
20 <th><%=l(:label_project)%></th>
20 <th><%=l(:label_project)%></th>
21 <th><%=l(:field_description)%></th>
21 <th><%=l(:field_description)%></th>
22 <th><%=l(:field_is_public)%></th>
22 <th><%=l(:field_is_public)%></th>
23 <th><%=l(:field_created_on)%></th>
23 <th><%=l(:field_created_on)%></th>
24 <th></th>
24 <th></th>
25 <th></th>
25 <th></th>
26 <th></th>
26 </tr></thead>
27 </tr></thead>
27 <tbody>
28 <tbody>
28 <% for project in @projects %>
29 <% for project in @projects %>
29 <tr class="<%= cycle("odd", "even") %> <%= css_project_classes(project) %>">
30 <tr class="<%= cycle("odd", "even") %> <%= css_project_classes(project) %>">
30 <td class="name" style="padding-left: <%= project.level %>em;"><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %></td>
31 <td class="name" style="padding-left: <%= project.level %>em;"><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %></td>
31 <td><%= textilizable project.short_description, :project => project %></td>
32 <td><%= textilizable project.short_description, :project => project %></td>
32 <td align="center"><%= image_tag 'true.png' if project.is_public? %></td>
33 <td align="center"><%= image_tag 'true.png' if project.is_public? %></td>
33 <td align="center"><%= format_date(project.created_on) %></td>
34 <td align="center"><%= format_date(project.created_on) %></td>
34 <td align="center" style="width:10%">
35 <td align="center" style="width:10%">
35 <small>
36 <small>
36 <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %>
37 <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %>
37 <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %>
38 <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %>
38 </small>
39 </small>
39 </td>
40 </td>
40 <td align="center" style="width:10%">
41 <td align="center" style="width:10%">
42 <%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %>
43 </td>
44 <td align="center" style="width:10%">
41 <small><%= link_to(l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => 'icon icon-del') %></small>
45 <small><%= link_to(l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => 'icon icon-del') %></small>
42 </td>
46 </td>
43 </tr>
47 </tr>
44 <% end %>
48 <% end %>
45 </tbody>
49 </tbody>
46 </table>
50 </table>
47
51
48 <% html_title(l(:label_project_plural)) -%>
52 <% html_title(l(:label_project_plural)) -%>
@@ -1,795 +1,796
1 en:
1 en:
2 date:
2 date:
3 formats:
3 formats:
4 # Use the strftime parameters for formats.
4 # Use the strftime parameters for formats.
5 # When no format has been given, it uses default.
5 # When no format has been given, it uses default.
6 # You can provide other formats here if you like!
6 # You can provide other formats here if you like!
7 default: "%m/%d/%Y"
7 default: "%m/%d/%Y"
8 short: "%b %d"
8 short: "%b %d"
9 long: "%B %d, %Y"
9 long: "%B %d, %Y"
10
10
11 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
11 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
12 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
12 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
13
13
14 # Don't forget the nil at the beginning; there's no such thing as a 0th month
14 # Don't forget the nil at the beginning; there's no such thing as a 0th month
15 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
15 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
16 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
16 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
17 # Used in date_select and datime_select.
17 # Used in date_select and datime_select.
18 order: [ :year, :month, :day ]
18 order: [ :year, :month, :day ]
19
19
20 time:
20 time:
21 formats:
21 formats:
22 default: "%m/%d/%Y %I:%M %p"
22 default: "%m/%d/%Y %I:%M %p"
23 time: "%I:%M %p"
23 time: "%I:%M %p"
24 short: "%d %b %H:%M"
24 short: "%d %b %H:%M"
25 long: "%B %d, %Y %H:%M"
25 long: "%B %d, %Y %H:%M"
26 am: "am"
26 am: "am"
27 pm: "pm"
27 pm: "pm"
28
28
29 datetime:
29 datetime:
30 distance_in_words:
30 distance_in_words:
31 half_a_minute: "half a minute"
31 half_a_minute: "half a minute"
32 less_than_x_seconds:
32 less_than_x_seconds:
33 one: "less than 1 second"
33 one: "less than 1 second"
34 other: "less than {{count}} seconds"
34 other: "less than {{count}} seconds"
35 x_seconds:
35 x_seconds:
36 one: "1 second"
36 one: "1 second"
37 other: "{{count}} seconds"
37 other: "{{count}} seconds"
38 less_than_x_minutes:
38 less_than_x_minutes:
39 one: "less than a minute"
39 one: "less than a minute"
40 other: "less than {{count}} minutes"
40 other: "less than {{count}} minutes"
41 x_minutes:
41 x_minutes:
42 one: "1 minute"
42 one: "1 minute"
43 other: "{{count}} minutes"
43 other: "{{count}} minutes"
44 about_x_hours:
44 about_x_hours:
45 one: "about 1 hour"
45 one: "about 1 hour"
46 other: "about {{count}} hours"
46 other: "about {{count}} hours"
47 x_days:
47 x_days:
48 one: "1 day"
48 one: "1 day"
49 other: "{{count}} days"
49 other: "{{count}} days"
50 about_x_months:
50 about_x_months:
51 one: "about 1 month"
51 one: "about 1 month"
52 other: "about {{count}} months"
52 other: "about {{count}} months"
53 x_months:
53 x_months:
54 one: "1 month"
54 one: "1 month"
55 other: "{{count}} months"
55 other: "{{count}} months"
56 about_x_years:
56 about_x_years:
57 one: "about 1 year"
57 one: "about 1 year"
58 other: "about {{count}} years"
58 other: "about {{count}} years"
59 over_x_years:
59 over_x_years:
60 one: "over 1 year"
60 one: "over 1 year"
61 other: "over {{count}} years"
61 other: "over {{count}} years"
62
62
63 # Used in array.to_sentence.
63 # Used in array.to_sentence.
64 support:
64 support:
65 array:
65 array:
66 sentence_connector: "and"
66 sentence_connector: "and"
67 skip_last_comma: false
67 skip_last_comma: false
68
68
69 activerecord:
69 activerecord:
70 errors:
70 errors:
71 messages:
71 messages:
72 inclusion: "is not included in the list"
72 inclusion: "is not included in the list"
73 exclusion: "is reserved"
73 exclusion: "is reserved"
74 invalid: "is invalid"
74 invalid: "is invalid"
75 confirmation: "doesn't match confirmation"
75 confirmation: "doesn't match confirmation"
76 accepted: "must be accepted"
76 accepted: "must be accepted"
77 empty: "can't be empty"
77 empty: "can't be empty"
78 blank: "can't be blank"
78 blank: "can't be blank"
79 too_long: "is too long (maximum is {{count}} characters)"
79 too_long: "is too long (maximum is {{count}} characters)"
80 too_short: "is too short (minimum is {{count}} characters)"
80 too_short: "is too short (minimum is {{count}} characters)"
81 wrong_length: "is the wrong length (should be {{count}} characters)"
81 wrong_length: "is the wrong length (should be {{count}} characters)"
82 taken: "has already been taken"
82 taken: "has already been taken"
83 not_a_number: "is not a number"
83 not_a_number: "is not a number"
84 not_a_date: "is not a valid date"
84 not_a_date: "is not a valid date"
85 greater_than: "must be greater than {{count}}"
85 greater_than: "must be greater than {{count}}"
86 greater_than_or_equal_to: "must be greater than or equal to {{count}}"
86 greater_than_or_equal_to: "must be greater than or equal to {{count}}"
87 equal_to: "must be equal to {{count}}"
87 equal_to: "must be equal to {{count}}"
88 less_than: "must be less than {{count}}"
88 less_than: "must be less than {{count}}"
89 less_than_or_equal_to: "must be less than or equal to {{count}}"
89 less_than_or_equal_to: "must be less than or equal to {{count}}"
90 odd: "must be odd"
90 odd: "must be odd"
91 even: "must be even"
91 even: "must be even"
92 greater_than_start_date: "must be greater than start date"
92 greater_than_start_date: "must be greater than start date"
93 not_same_project: "doesn't belong to the same project"
93 not_same_project: "doesn't belong to the same project"
94 circular_dependency: "This relation would create a circular dependency"
94 circular_dependency: "This relation would create a circular dependency"
95
95
96 actionview_instancetag_blank_option: Please select
96 actionview_instancetag_blank_option: Please select
97
97
98 general_text_No: 'No'
98 general_text_No: 'No'
99 general_text_Yes: 'Yes'
99 general_text_Yes: 'Yes'
100 general_text_no: 'no'
100 general_text_no: 'no'
101 general_text_yes: 'yes'
101 general_text_yes: 'yes'
102 general_lang_name: 'English'
102 general_lang_name: 'English'
103 general_csv_separator: ','
103 general_csv_separator: ','
104 general_csv_decimal_separator: '.'
104 general_csv_decimal_separator: '.'
105 general_csv_encoding: ISO-8859-1
105 general_csv_encoding: ISO-8859-1
106 general_pdf_encoding: ISO-8859-1
106 general_pdf_encoding: ISO-8859-1
107 general_first_day_of_week: '7'
107 general_first_day_of_week: '7'
108
108
109 notice_account_updated: Account was successfully updated.
109 notice_account_updated: Account was successfully updated.
110 notice_account_invalid_creditentials: Invalid user or password
110 notice_account_invalid_creditentials: Invalid user or password
111 notice_account_password_updated: Password was successfully updated.
111 notice_account_password_updated: Password was successfully updated.
112 notice_account_wrong_password: Wrong password
112 notice_account_wrong_password: Wrong password
113 notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
113 notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
114 notice_account_unknown_email: Unknown user.
114 notice_account_unknown_email: Unknown user.
115 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
115 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
116 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
116 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
117 notice_account_activated: Your account has been activated. You can now log in.
117 notice_account_activated: Your account has been activated. You can now log in.
118 notice_successful_create: Successful creation.
118 notice_successful_create: Successful creation.
119 notice_successful_update: Successful update.
119 notice_successful_update: Successful update.
120 notice_successful_delete: Successful deletion.
120 notice_successful_delete: Successful deletion.
121 notice_successful_connection: Successful connection.
121 notice_successful_connection: Successful connection.
122 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
122 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
123 notice_locking_conflict: Data has been updated by another user.
123 notice_locking_conflict: Data has been updated by another user.
124 notice_not_authorized: You are not authorized to access this page.
124 notice_not_authorized: You are not authorized to access this page.
125 notice_email_sent: "An email was sent to {{value}}"
125 notice_email_sent: "An email was sent to {{value}}"
126 notice_email_error: "An error occurred while sending mail ({{value}})"
126 notice_email_error: "An error occurred while sending mail ({{value}})"
127 notice_feeds_access_key_reseted: Your RSS access key was reset.
127 notice_feeds_access_key_reseted: Your RSS access key was reset.
128 notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
128 notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
129 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
129 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
130 notice_account_pending: "Your account was created and is now pending administrator approval."
130 notice_account_pending: "Your account was created and is now pending administrator approval."
131 notice_default_data_loaded: Default configuration successfully loaded.
131 notice_default_data_loaded: Default configuration successfully loaded.
132 notice_unable_delete_version: Unable to delete version.
132 notice_unable_delete_version: Unable to delete version.
133
133
134 error_can_t_load_default_data: "Default configuration could not be loaded: {{value}}"
134 error_can_t_load_default_data: "Default configuration could not be loaded: {{value}}"
135 error_scm_not_found: "The entry or revision was not found in the repository."
135 error_scm_not_found: "The entry or revision was not found in the repository."
136 error_scm_command_failed: "An error occurred when trying to access the repository: {{value}}"
136 error_scm_command_failed: "An error occurred when trying to access the repository: {{value}}"
137 error_scm_annotate: "The entry does not exist or can not be annotated."
137 error_scm_annotate: "The entry does not exist or can not be annotated."
138 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
138 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
139
139
140 warning_attachments_not_saved: "{{count}} file(s) could not be saved."
140 warning_attachments_not_saved: "{{count}} file(s) could not be saved."
141
141
142 mail_subject_lost_password: "Your {{value}} password"
142 mail_subject_lost_password: "Your {{value}} password"
143 mail_body_lost_password: 'To change your password, click on the following link:'
143 mail_body_lost_password: 'To change your password, click on the following link:'
144 mail_subject_register: "Your {{value}} account activation"
144 mail_subject_register: "Your {{value}} account activation"
145 mail_body_register: 'To activate your account, click on the following link:'
145 mail_body_register: 'To activate your account, click on the following link:'
146 mail_body_account_information_external: "You can use your {{value}} account to log in."
146 mail_body_account_information_external: "You can use your {{value}} account to log in."
147 mail_body_account_information: Your account information
147 mail_body_account_information: Your account information
148 mail_subject_account_activation_request: "{{value}} account activation request"
148 mail_subject_account_activation_request: "{{value}} account activation request"
149 mail_body_account_activation_request: "A new user ({{value}}) has registered. The account is pending your approval:"
149 mail_body_account_activation_request: "A new user ({{value}}) has registered. The account is pending your approval:"
150 mail_subject_reminder: "{{count}} issue(s) due in the next days"
150 mail_subject_reminder: "{{count}} issue(s) due in the next days"
151 mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
151 mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
152
152
153 gui_validation_error: 1 error
153 gui_validation_error: 1 error
154 gui_validation_error_plural: "{{count}} errors"
154 gui_validation_error_plural: "{{count}} errors"
155
155
156 field_name: Name
156 field_name: Name
157 field_description: Description
157 field_description: Description
158 field_summary: Summary
158 field_summary: Summary
159 field_is_required: Required
159 field_is_required: Required
160 field_firstname: Firstname
160 field_firstname: Firstname
161 field_lastname: Lastname
161 field_lastname: Lastname
162 field_mail: Email
162 field_mail: Email
163 field_filename: File
163 field_filename: File
164 field_filesize: Size
164 field_filesize: Size
165 field_downloads: Downloads
165 field_downloads: Downloads
166 field_author: Author
166 field_author: Author
167 field_created_on: Created
167 field_created_on: Created
168 field_updated_on: Updated
168 field_updated_on: Updated
169 field_field_format: Format
169 field_field_format: Format
170 field_is_for_all: For all projects
170 field_is_for_all: For all projects
171 field_possible_values: Possible values
171 field_possible_values: Possible values
172 field_regexp: Regular expression
172 field_regexp: Regular expression
173 field_min_length: Minimum length
173 field_min_length: Minimum length
174 field_max_length: Maximum length
174 field_max_length: Maximum length
175 field_value: Value
175 field_value: Value
176 field_category: Category
176 field_category: Category
177 field_title: Title
177 field_title: Title
178 field_project: Project
178 field_project: Project
179 field_issue: Issue
179 field_issue: Issue
180 field_status: Status
180 field_status: Status
181 field_notes: Notes
181 field_notes: Notes
182 field_is_closed: Issue closed
182 field_is_closed: Issue closed
183 field_is_default: Default value
183 field_is_default: Default value
184 field_tracker: Tracker
184 field_tracker: Tracker
185 field_subject: Subject
185 field_subject: Subject
186 field_due_date: Due date
186 field_due_date: Due date
187 field_assigned_to: Assigned to
187 field_assigned_to: Assigned to
188 field_priority: Priority
188 field_priority: Priority
189 field_fixed_version: Target version
189 field_fixed_version: Target version
190 field_user: User
190 field_user: User
191 field_role: Role
191 field_role: Role
192 field_homepage: Homepage
192 field_homepage: Homepage
193 field_is_public: Public
193 field_is_public: Public
194 field_parent: Subproject of
194 field_parent: Subproject of
195 field_is_in_chlog: Issues displayed in changelog
195 field_is_in_chlog: Issues displayed in changelog
196 field_is_in_roadmap: Issues displayed in roadmap
196 field_is_in_roadmap: Issues displayed in roadmap
197 field_login: Login
197 field_login: Login
198 field_mail_notification: Email notifications
198 field_mail_notification: Email notifications
199 field_admin: Administrator
199 field_admin: Administrator
200 field_last_login_on: Last connection
200 field_last_login_on: Last connection
201 field_language: Language
201 field_language: Language
202 field_effective_date: Date
202 field_effective_date: Date
203 field_password: Password
203 field_password: Password
204 field_new_password: New password
204 field_new_password: New password
205 field_password_confirmation: Confirmation
205 field_password_confirmation: Confirmation
206 field_version: Version
206 field_version: Version
207 field_type: Type
207 field_type: Type
208 field_host: Host
208 field_host: Host
209 field_port: Port
209 field_port: Port
210 field_account: Account
210 field_account: Account
211 field_base_dn: Base DN
211 field_base_dn: Base DN
212 field_attr_login: Login attribute
212 field_attr_login: Login attribute
213 field_attr_firstname: Firstname attribute
213 field_attr_firstname: Firstname attribute
214 field_attr_lastname: Lastname attribute
214 field_attr_lastname: Lastname attribute
215 field_attr_mail: Email attribute
215 field_attr_mail: Email attribute
216 field_onthefly: On-the-fly user creation
216 field_onthefly: On-the-fly user creation
217 field_start_date: Start
217 field_start_date: Start
218 field_done_ratio: % Done
218 field_done_ratio: % Done
219 field_auth_source: Authentication mode
219 field_auth_source: Authentication mode
220 field_hide_mail: Hide my email address
220 field_hide_mail: Hide my email address
221 field_comments: Comment
221 field_comments: Comment
222 field_url: URL
222 field_url: URL
223 field_start_page: Start page
223 field_start_page: Start page
224 field_subproject: Subproject
224 field_subproject: Subproject
225 field_hours: Hours
225 field_hours: Hours
226 field_activity: Activity
226 field_activity: Activity
227 field_spent_on: Date
227 field_spent_on: Date
228 field_identifier: Identifier
228 field_identifier: Identifier
229 field_is_filter: Used as a filter
229 field_is_filter: Used as a filter
230 field_issue_to_id: Related issue
230 field_issue_to_id: Related issue
231 field_delay: Delay
231 field_delay: Delay
232 field_assignable: Issues can be assigned to this role
232 field_assignable: Issues can be assigned to this role
233 field_redirect_existing_links: Redirect existing links
233 field_redirect_existing_links: Redirect existing links
234 field_estimated_hours: Estimated time
234 field_estimated_hours: Estimated time
235 field_column_names: Columns
235 field_column_names: Columns
236 field_time_zone: Time zone
236 field_time_zone: Time zone
237 field_searchable: Searchable
237 field_searchable: Searchable
238 field_default_value: Default value
238 field_default_value: Default value
239 field_comments_sorting: Display comments
239 field_comments_sorting: Display comments
240 field_parent_title: Parent page
240 field_parent_title: Parent page
241 field_editable: Editable
241 field_editable: Editable
242 field_watcher: Watcher
242 field_watcher: Watcher
243 field_identity_url: OpenID URL
243 field_identity_url: OpenID URL
244 field_content: Content
244 field_content: Content
245 field_group_by: Group results by
245 field_group_by: Group results by
246
246
247 setting_app_title: Application title
247 setting_app_title: Application title
248 setting_app_subtitle: Application subtitle
248 setting_app_subtitle: Application subtitle
249 setting_welcome_text: Welcome text
249 setting_welcome_text: Welcome text
250 setting_default_language: Default language
250 setting_default_language: Default language
251 setting_login_required: Authentication required
251 setting_login_required: Authentication required
252 setting_self_registration: Self-registration
252 setting_self_registration: Self-registration
253 setting_attachment_max_size: Attachment max. size
253 setting_attachment_max_size: Attachment max. size
254 setting_issues_export_limit: Issues export limit
254 setting_issues_export_limit: Issues export limit
255 setting_mail_from: Emission email address
255 setting_mail_from: Emission email address
256 setting_bcc_recipients: Blind carbon copy recipients (bcc)
256 setting_bcc_recipients: Blind carbon copy recipients (bcc)
257 setting_plain_text_mail: Plain text mail (no HTML)
257 setting_plain_text_mail: Plain text mail (no HTML)
258 setting_host_name: Host name and path
258 setting_host_name: Host name and path
259 setting_text_formatting: Text formatting
259 setting_text_formatting: Text formatting
260 setting_wiki_compression: Wiki history compression
260 setting_wiki_compression: Wiki history compression
261 setting_feeds_limit: Feed content limit
261 setting_feeds_limit: Feed content limit
262 setting_default_projects_public: New projects are public by default
262 setting_default_projects_public: New projects are public by default
263 setting_autofetch_changesets: Autofetch commits
263 setting_autofetch_changesets: Autofetch commits
264 setting_sys_api_enabled: Enable WS for repository management
264 setting_sys_api_enabled: Enable WS for repository management
265 setting_commit_ref_keywords: Referencing keywords
265 setting_commit_ref_keywords: Referencing keywords
266 setting_commit_fix_keywords: Fixing keywords
266 setting_commit_fix_keywords: Fixing keywords
267 setting_autologin: Autologin
267 setting_autologin: Autologin
268 setting_date_format: Date format
268 setting_date_format: Date format
269 setting_time_format: Time format
269 setting_time_format: Time format
270 setting_cross_project_issue_relations: Allow cross-project issue relations
270 setting_cross_project_issue_relations: Allow cross-project issue relations
271 setting_issue_list_default_columns: Default columns displayed on the issue list
271 setting_issue_list_default_columns: Default columns displayed on the issue list
272 setting_repositories_encodings: Repositories encodings
272 setting_repositories_encodings: Repositories encodings
273 setting_commit_logs_encoding: Commit messages encoding
273 setting_commit_logs_encoding: Commit messages encoding
274 setting_emails_footer: Emails footer
274 setting_emails_footer: Emails footer
275 setting_protocol: Protocol
275 setting_protocol: Protocol
276 setting_per_page_options: Objects per page options
276 setting_per_page_options: Objects per page options
277 setting_user_format: Users display format
277 setting_user_format: Users display format
278 setting_activity_days_default: Days displayed on project activity
278 setting_activity_days_default: Days displayed on project activity
279 setting_display_subprojects_issues: Display subprojects issues on main projects by default
279 setting_display_subprojects_issues: Display subprojects issues on main projects by default
280 setting_enabled_scm: Enabled SCM
280 setting_enabled_scm: Enabled SCM
281 setting_mail_handler_api_enabled: Enable WS for incoming emails
281 setting_mail_handler_api_enabled: Enable WS for incoming emails
282 setting_mail_handler_api_key: API key
282 setting_mail_handler_api_key: API key
283 setting_sequential_project_identifiers: Generate sequential project identifiers
283 setting_sequential_project_identifiers: Generate sequential project identifiers
284 setting_gravatar_enabled: Use Gravatar user icons
284 setting_gravatar_enabled: Use Gravatar user icons
285 setting_diff_max_lines_displayed: Max number of diff lines displayed
285 setting_diff_max_lines_displayed: Max number of diff lines displayed
286 setting_file_max_size_displayed: Max size of text files displayed inline
286 setting_file_max_size_displayed: Max size of text files displayed inline
287 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
287 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
288 setting_openid: Allow OpenID login and registration
288 setting_openid: Allow OpenID login and registration
289 setting_password_min_length: Minimum password length
289 setting_password_min_length: Minimum password length
290
290
291 permission_edit_project: Edit project
291 permission_edit_project: Edit project
292 permission_select_project_modules: Select project modules
292 permission_select_project_modules: Select project modules
293 permission_manage_members: Manage members
293 permission_manage_members: Manage members
294 permission_manage_versions: Manage versions
294 permission_manage_versions: Manage versions
295 permission_manage_categories: Manage issue categories
295 permission_manage_categories: Manage issue categories
296 permission_add_issues: Add issues
296 permission_add_issues: Add issues
297 permission_edit_issues: Edit issues
297 permission_edit_issues: Edit issues
298 permission_manage_issue_relations: Manage issue relations
298 permission_manage_issue_relations: Manage issue relations
299 permission_add_issue_notes: Add notes
299 permission_add_issue_notes: Add notes
300 permission_edit_issue_notes: Edit notes
300 permission_edit_issue_notes: Edit notes
301 permission_edit_own_issue_notes: Edit own notes
301 permission_edit_own_issue_notes: Edit own notes
302 permission_move_issues: Move issues
302 permission_move_issues: Move issues
303 permission_delete_issues: Delete issues
303 permission_delete_issues: Delete issues
304 permission_manage_public_queries: Manage public queries
304 permission_manage_public_queries: Manage public queries
305 permission_save_queries: Save queries
305 permission_save_queries: Save queries
306 permission_view_gantt: View gantt chart
306 permission_view_gantt: View gantt chart
307 permission_view_calendar: View calender
307 permission_view_calendar: View calender
308 permission_view_issue_watchers: View watchers list
308 permission_view_issue_watchers: View watchers list
309 permission_add_issue_watchers: Add watchers
309 permission_add_issue_watchers: Add watchers
310 permission_log_time: Log spent time
310 permission_log_time: Log spent time
311 permission_view_time_entries: View spent time
311 permission_view_time_entries: View spent time
312 permission_edit_time_entries: Edit time logs
312 permission_edit_time_entries: Edit time logs
313 permission_edit_own_time_entries: Edit own time logs
313 permission_edit_own_time_entries: Edit own time logs
314 permission_manage_news: Manage news
314 permission_manage_news: Manage news
315 permission_comment_news: Comment news
315 permission_comment_news: Comment news
316 permission_manage_documents: Manage documents
316 permission_manage_documents: Manage documents
317 permission_view_documents: View documents
317 permission_view_documents: View documents
318 permission_manage_files: Manage files
318 permission_manage_files: Manage files
319 permission_view_files: View files
319 permission_view_files: View files
320 permission_manage_wiki: Manage wiki
320 permission_manage_wiki: Manage wiki
321 permission_rename_wiki_pages: Rename wiki pages
321 permission_rename_wiki_pages: Rename wiki pages
322 permission_delete_wiki_pages: Delete wiki pages
322 permission_delete_wiki_pages: Delete wiki pages
323 permission_view_wiki_pages: View wiki
323 permission_view_wiki_pages: View wiki
324 permission_view_wiki_edits: View wiki history
324 permission_view_wiki_edits: View wiki history
325 permission_edit_wiki_pages: Edit wiki pages
325 permission_edit_wiki_pages: Edit wiki pages
326 permission_delete_wiki_pages_attachments: Delete attachments
326 permission_delete_wiki_pages_attachments: Delete attachments
327 permission_protect_wiki_pages: Protect wiki pages
327 permission_protect_wiki_pages: Protect wiki pages
328 permission_manage_repository: Manage repository
328 permission_manage_repository: Manage repository
329 permission_browse_repository: Browse repository
329 permission_browse_repository: Browse repository
330 permission_view_changesets: View changesets
330 permission_view_changesets: View changesets
331 permission_commit_access: Commit access
331 permission_commit_access: Commit access
332 permission_manage_boards: Manage boards
332 permission_manage_boards: Manage boards
333 permission_view_messages: View messages
333 permission_view_messages: View messages
334 permission_add_messages: Post messages
334 permission_add_messages: Post messages
335 permission_edit_messages: Edit messages
335 permission_edit_messages: Edit messages
336 permission_edit_own_messages: Edit own messages
336 permission_edit_own_messages: Edit own messages
337 permission_delete_messages: Delete messages
337 permission_delete_messages: Delete messages
338 permission_delete_own_messages: Delete own messages
338 permission_delete_own_messages: Delete own messages
339
339
340 project_module_issue_tracking: Issue tracking
340 project_module_issue_tracking: Issue tracking
341 project_module_time_tracking: Time tracking
341 project_module_time_tracking: Time tracking
342 project_module_news: News
342 project_module_news: News
343 project_module_documents: Documents
343 project_module_documents: Documents
344 project_module_files: Files
344 project_module_files: Files
345 project_module_wiki: Wiki
345 project_module_wiki: Wiki
346 project_module_repository: Repository
346 project_module_repository: Repository
347 project_module_boards: Boards
347 project_module_boards: Boards
348
348
349 label_user: User
349 label_user: User
350 label_user_plural: Users
350 label_user_plural: Users
351 label_user_new: New user
351 label_user_new: New user
352 label_project: Project
352 label_project: Project
353 label_project_new: New project
353 label_project_new: New project
354 label_project_copy: Copy project
354 label_project_plural: Projects
355 label_project_plural: Projects
355 label_x_projects:
356 label_x_projects:
356 zero: no projects
357 zero: no projects
357 one: 1 project
358 one: 1 project
358 other: "{{count}} projects"
359 other: "{{count}} projects"
359 label_project_all: All Projects
360 label_project_all: All Projects
360 label_project_latest: Latest projects
361 label_project_latest: Latest projects
361 label_issue: Issue
362 label_issue: Issue
362 label_issue_new: New issue
363 label_issue_new: New issue
363 label_issue_plural: Issues
364 label_issue_plural: Issues
364 label_issue_view_all: View all issues
365 label_issue_view_all: View all issues
365 label_issues_by: "Issues by {{value}}"
366 label_issues_by: "Issues by {{value}}"
366 label_issue_added: Issue added
367 label_issue_added: Issue added
367 label_issue_updated: Issue updated
368 label_issue_updated: Issue updated
368 label_document: Document
369 label_document: Document
369 label_document_new: New document
370 label_document_new: New document
370 label_document_plural: Documents
371 label_document_plural: Documents
371 label_document_added: Document added
372 label_document_added: Document added
372 label_role: Role
373 label_role: Role
373 label_role_plural: Roles
374 label_role_plural: Roles
374 label_role_new: New role
375 label_role_new: New role
375 label_role_and_permissions: Roles and permissions
376 label_role_and_permissions: Roles and permissions
376 label_member: Member
377 label_member: Member
377 label_member_new: New member
378 label_member_new: New member
378 label_member_plural: Members
379 label_member_plural: Members
379 label_tracker: Tracker
380 label_tracker: Tracker
380 label_tracker_plural: Trackers
381 label_tracker_plural: Trackers
381 label_tracker_new: New tracker
382 label_tracker_new: New tracker
382 label_workflow: Workflow
383 label_workflow: Workflow
383 label_issue_status: Issue status
384 label_issue_status: Issue status
384 label_issue_status_plural: Issue statuses
385 label_issue_status_plural: Issue statuses
385 label_issue_status_new: New status
386 label_issue_status_new: New status
386 label_issue_category: Issue category
387 label_issue_category: Issue category
387 label_issue_category_plural: Issue categories
388 label_issue_category_plural: Issue categories
388 label_issue_category_new: New category
389 label_issue_category_new: New category
389 label_custom_field: Custom field
390 label_custom_field: Custom field
390 label_custom_field_plural: Custom fields
391 label_custom_field_plural: Custom fields
391 label_custom_field_new: New custom field
392 label_custom_field_new: New custom field
392 label_enumerations: Enumerations
393 label_enumerations: Enumerations
393 label_enumeration_new: New value
394 label_enumeration_new: New value
394 label_information: Information
395 label_information: Information
395 label_information_plural: Information
396 label_information_plural: Information
396 label_please_login: Please log in
397 label_please_login: Please log in
397 label_register: Register
398 label_register: Register
398 label_login_with_open_id_option: or login with OpenID
399 label_login_with_open_id_option: or login with OpenID
399 label_password_lost: Lost password
400 label_password_lost: Lost password
400 label_home: Home
401 label_home: Home
401 label_my_page: My page
402 label_my_page: My page
402 label_my_account: My account
403 label_my_account: My account
403 label_my_projects: My projects
404 label_my_projects: My projects
404 label_administration: Administration
405 label_administration: Administration
405 label_login: Sign in
406 label_login: Sign in
406 label_logout: Sign out
407 label_logout: Sign out
407 label_help: Help
408 label_help: Help
408 label_reported_issues: Reported issues
409 label_reported_issues: Reported issues
409 label_assigned_to_me_issues: Issues assigned to me
410 label_assigned_to_me_issues: Issues assigned to me
410 label_last_login: Last connection
411 label_last_login: Last connection
411 label_registered_on: Registered on
412 label_registered_on: Registered on
412 label_activity: Activity
413 label_activity: Activity
413 label_overall_activity: Overall activity
414 label_overall_activity: Overall activity
414 label_user_activity: "{{value}}'s activity"
415 label_user_activity: "{{value}}'s activity"
415 label_new: New
416 label_new: New
416 label_logged_as: Logged in as
417 label_logged_as: Logged in as
417 label_environment: Environment
418 label_environment: Environment
418 label_authentication: Authentication
419 label_authentication: Authentication
419 label_auth_source: Authentication mode
420 label_auth_source: Authentication mode
420 label_auth_source_new: New authentication mode
421 label_auth_source_new: New authentication mode
421 label_auth_source_plural: Authentication modes
422 label_auth_source_plural: Authentication modes
422 label_subproject_plural: Subprojects
423 label_subproject_plural: Subprojects
423 label_and_its_subprojects: "{{value}} and its subprojects"
424 label_and_its_subprojects: "{{value}} and its subprojects"
424 label_min_max_length: Min - Max length
425 label_min_max_length: Min - Max length
425 label_list: List
426 label_list: List
426 label_date: Date
427 label_date: Date
427 label_integer: Integer
428 label_integer: Integer
428 label_float: Float
429 label_float: Float
429 label_boolean: Boolean
430 label_boolean: Boolean
430 label_string: Text
431 label_string: Text
431 label_text: Long text
432 label_text: Long text
432 label_attribute: Attribute
433 label_attribute: Attribute
433 label_attribute_plural: Attributes
434 label_attribute_plural: Attributes
434 label_download: "{{count}} Download"
435 label_download: "{{count}} Download"
435 label_download_plural: "{{count}} Downloads"
436 label_download_plural: "{{count}} Downloads"
436 label_no_data: No data to display
437 label_no_data: No data to display
437 label_change_status: Change status
438 label_change_status: Change status
438 label_history: History
439 label_history: History
439 label_attachment: File
440 label_attachment: File
440 label_attachment_new: New file
441 label_attachment_new: New file
441 label_attachment_delete: Delete file
442 label_attachment_delete: Delete file
442 label_attachment_plural: Files
443 label_attachment_plural: Files
443 label_file_added: File added
444 label_file_added: File added
444 label_report: Report
445 label_report: Report
445 label_report_plural: Reports
446 label_report_plural: Reports
446 label_news: News
447 label_news: News
447 label_news_new: Add news
448 label_news_new: Add news
448 label_news_plural: News
449 label_news_plural: News
449 label_news_latest: Latest news
450 label_news_latest: Latest news
450 label_news_view_all: View all news
451 label_news_view_all: View all news
451 label_news_added: News added
452 label_news_added: News added
452 label_change_log: Change log
453 label_change_log: Change log
453 label_settings: Settings
454 label_settings: Settings
454 label_overview: Overview
455 label_overview: Overview
455 label_version: Version
456 label_version: Version
456 label_version_new: New version
457 label_version_new: New version
457 label_version_plural: Versions
458 label_version_plural: Versions
458 label_confirmation: Confirmation
459 label_confirmation: Confirmation
459 label_export_to: 'Also available in:'
460 label_export_to: 'Also available in:'
460 label_read: Read...
461 label_read: Read...
461 label_public_projects: Public projects
462 label_public_projects: Public projects
462 label_open_issues: open
463 label_open_issues: open
463 label_open_issues_plural: open
464 label_open_issues_plural: open
464 label_closed_issues: closed
465 label_closed_issues: closed
465 label_closed_issues_plural: closed
466 label_closed_issues_plural: closed
466 label_x_open_issues_abbr_on_total:
467 label_x_open_issues_abbr_on_total:
467 zero: 0 open / {{total}}
468 zero: 0 open / {{total}}
468 one: 1 open / {{total}}
469 one: 1 open / {{total}}
469 other: "{{count}} open / {{total}}"
470 other: "{{count}} open / {{total}}"
470 label_x_open_issues_abbr:
471 label_x_open_issues_abbr:
471 zero: 0 open
472 zero: 0 open
472 one: 1 open
473 one: 1 open
473 other: "{{count}} open"
474 other: "{{count}} open"
474 label_x_closed_issues_abbr:
475 label_x_closed_issues_abbr:
475 zero: 0 closed
476 zero: 0 closed
476 one: 1 closed
477 one: 1 closed
477 other: "{{count}} closed"
478 other: "{{count}} closed"
478 label_total: Total
479 label_total: Total
479 label_permissions: Permissions
480 label_permissions: Permissions
480 label_current_status: Current status
481 label_current_status: Current status
481 label_new_statuses_allowed: New statuses allowed
482 label_new_statuses_allowed: New statuses allowed
482 label_all: all
483 label_all: all
483 label_none: none
484 label_none: none
484 label_nobody: nobody
485 label_nobody: nobody
485 label_next: Next
486 label_next: Next
486 label_previous: Previous
487 label_previous: Previous
487 label_used_by: Used by
488 label_used_by: Used by
488 label_details: Details
489 label_details: Details
489 label_add_note: Add a note
490 label_add_note: Add a note
490 label_per_page: Per page
491 label_per_page: Per page
491 label_calendar: Calendar
492 label_calendar: Calendar
492 label_months_from: months from
493 label_months_from: months from
493 label_gantt: Gantt
494 label_gantt: Gantt
494 label_internal: Internal
495 label_internal: Internal
495 label_last_changes: "last {{count}} changes"
496 label_last_changes: "last {{count}} changes"
496 label_change_view_all: View all changes
497 label_change_view_all: View all changes
497 label_personalize_page: Personalize this page
498 label_personalize_page: Personalize this page
498 label_comment: Comment
499 label_comment: Comment
499 label_comment_plural: Comments
500 label_comment_plural: Comments
500 label_x_comments:
501 label_x_comments:
501 zero: no comments
502 zero: no comments
502 one: 1 comment
503 one: 1 comment
503 other: "{{count}} comments"
504 other: "{{count}} comments"
504 label_comment_add: Add a comment
505 label_comment_add: Add a comment
505 label_comment_added: Comment added
506 label_comment_added: Comment added
506 label_comment_delete: Delete comments
507 label_comment_delete: Delete comments
507 label_query: Custom query
508 label_query: Custom query
508 label_query_plural: Custom queries
509 label_query_plural: Custom queries
509 label_query_new: New query
510 label_query_new: New query
510 label_filter_add: Add filter
511 label_filter_add: Add filter
511 label_filter_plural: Filters
512 label_filter_plural: Filters
512 label_equals: is
513 label_equals: is
513 label_not_equals: is not
514 label_not_equals: is not
514 label_in_less_than: in less than
515 label_in_less_than: in less than
515 label_in_more_than: in more than
516 label_in_more_than: in more than
516 label_greater_or_equal: '>='
517 label_greater_or_equal: '>='
517 label_less_or_equal: '<='
518 label_less_or_equal: '<='
518 label_in: in
519 label_in: in
519 label_today: today
520 label_today: today
520 label_all_time: all time
521 label_all_time: all time
521 label_yesterday: yesterday
522 label_yesterday: yesterday
522 label_this_week: this week
523 label_this_week: this week
523 label_last_week: last week
524 label_last_week: last week
524 label_last_n_days: "last {{count}} days"
525 label_last_n_days: "last {{count}} days"
525 label_this_month: this month
526 label_this_month: this month
526 label_last_month: last month
527 label_last_month: last month
527 label_this_year: this year
528 label_this_year: this year
528 label_date_range: Date range
529 label_date_range: Date range
529 label_less_than_ago: less than days ago
530 label_less_than_ago: less than days ago
530 label_more_than_ago: more than days ago
531 label_more_than_ago: more than days ago
531 label_ago: days ago
532 label_ago: days ago
532 label_contains: contains
533 label_contains: contains
533 label_not_contains: doesn't contain
534 label_not_contains: doesn't contain
534 label_day_plural: days
535 label_day_plural: days
535 label_repository: Repository
536 label_repository: Repository
536 label_repository_plural: Repositories
537 label_repository_plural: Repositories
537 label_browse: Browse
538 label_browse: Browse
538 label_modification: "{{count}} change"
539 label_modification: "{{count}} change"
539 label_modification_plural: "{{count}} changes"
540 label_modification_plural: "{{count}} changes"
540 label_revision: Revision
541 label_revision: Revision
541 label_revision_plural: Revisions
542 label_revision_plural: Revisions
542 label_associated_revisions: Associated revisions
543 label_associated_revisions: Associated revisions
543 label_added: added
544 label_added: added
544 label_modified: modified
545 label_modified: modified
545 label_copied: copied
546 label_copied: copied
546 label_renamed: renamed
547 label_renamed: renamed
547 label_deleted: deleted
548 label_deleted: deleted
548 label_latest_revision: Latest revision
549 label_latest_revision: Latest revision
549 label_latest_revision_plural: Latest revisions
550 label_latest_revision_plural: Latest revisions
550 label_view_revisions: View revisions
551 label_view_revisions: View revisions
551 label_max_size: Maximum size
552 label_max_size: Maximum size
552 label_sort_highest: Move to top
553 label_sort_highest: Move to top
553 label_sort_higher: Move up
554 label_sort_higher: Move up
554 label_sort_lower: Move down
555 label_sort_lower: Move down
555 label_sort_lowest: Move to bottom
556 label_sort_lowest: Move to bottom
556 label_roadmap: Roadmap
557 label_roadmap: Roadmap
557 label_roadmap_due_in: "Due in {{value}}"
558 label_roadmap_due_in: "Due in {{value}}"
558 label_roadmap_overdue: "{{value}} late"
559 label_roadmap_overdue: "{{value}} late"
559 label_roadmap_no_issues: No issues for this version
560 label_roadmap_no_issues: No issues for this version
560 label_search: Search
561 label_search: Search
561 label_result_plural: Results
562 label_result_plural: Results
562 label_all_words: All words
563 label_all_words: All words
563 label_wiki: Wiki
564 label_wiki: Wiki
564 label_wiki_edit: Wiki edit
565 label_wiki_edit: Wiki edit
565 label_wiki_edit_plural: Wiki edits
566 label_wiki_edit_plural: Wiki edits
566 label_wiki_page: Wiki page
567 label_wiki_page: Wiki page
567 label_wiki_page_plural: Wiki pages
568 label_wiki_page_plural: Wiki pages
568 label_index_by_title: Index by title
569 label_index_by_title: Index by title
569 label_index_by_date: Index by date
570 label_index_by_date: Index by date
570 label_current_version: Current version
571 label_current_version: Current version
571 label_preview: Preview
572 label_preview: Preview
572 label_feed_plural: Feeds
573 label_feed_plural: Feeds
573 label_changes_details: Details of all changes
574 label_changes_details: Details of all changes
574 label_issue_tracking: Issue tracking
575 label_issue_tracking: Issue tracking
575 label_spent_time: Spent time
576 label_spent_time: Spent time
576 label_f_hour: "{{value}} hour"
577 label_f_hour: "{{value}} hour"
577 label_f_hour_plural: "{{value}} hours"
578 label_f_hour_plural: "{{value}} hours"
578 label_time_tracking: Time tracking
579 label_time_tracking: Time tracking
579 label_change_plural: Changes
580 label_change_plural: Changes
580 label_statistics: Statistics
581 label_statistics: Statistics
581 label_commits_per_month: Commits per month
582 label_commits_per_month: Commits per month
582 label_commits_per_author: Commits per author
583 label_commits_per_author: Commits per author
583 label_view_diff: View differences
584 label_view_diff: View differences
584 label_diff_inline: inline
585 label_diff_inline: inline
585 label_diff_side_by_side: side by side
586 label_diff_side_by_side: side by side
586 label_options: Options
587 label_options: Options
587 label_copy_workflow_from: Copy workflow from
588 label_copy_workflow_from: Copy workflow from
588 label_permissions_report: Permissions report
589 label_permissions_report: Permissions report
589 label_watched_issues: Watched issues
590 label_watched_issues: Watched issues
590 label_related_issues: Related issues
591 label_related_issues: Related issues
591 label_applied_status: Applied status
592 label_applied_status: Applied status
592 label_loading: Loading...
593 label_loading: Loading...
593 label_relation_new: New relation
594 label_relation_new: New relation
594 label_relation_delete: Delete relation
595 label_relation_delete: Delete relation
595 label_relates_to: related to
596 label_relates_to: related to
596 label_duplicates: duplicates
597 label_duplicates: duplicates
597 label_duplicated_by: duplicated by
598 label_duplicated_by: duplicated by
598 label_blocks: blocks
599 label_blocks: blocks
599 label_blocked_by: blocked by
600 label_blocked_by: blocked by
600 label_precedes: precedes
601 label_precedes: precedes
601 label_follows: follows
602 label_follows: follows
602 label_end_to_start: end to start
603 label_end_to_start: end to start
603 label_end_to_end: end to end
604 label_end_to_end: end to end
604 label_start_to_start: start to start
605 label_start_to_start: start to start
605 label_start_to_end: start to end
606 label_start_to_end: start to end
606 label_stay_logged_in: Stay logged in
607 label_stay_logged_in: Stay logged in
607 label_disabled: disabled
608 label_disabled: disabled
608 label_show_completed_versions: Show completed versions
609 label_show_completed_versions: Show completed versions
609 label_me: me
610 label_me: me
610 label_board: Forum
611 label_board: Forum
611 label_board_new: New forum
612 label_board_new: New forum
612 label_board_plural: Forums
613 label_board_plural: Forums
613 label_topic_plural: Topics
614 label_topic_plural: Topics
614 label_message_plural: Messages
615 label_message_plural: Messages
615 label_message_last: Last message
616 label_message_last: Last message
616 label_message_new: New message
617 label_message_new: New message
617 label_message_posted: Message added
618 label_message_posted: Message added
618 label_reply_plural: Replies
619 label_reply_plural: Replies
619 label_send_information: Send account information to the user
620 label_send_information: Send account information to the user
620 label_year: Year
621 label_year: Year
621 label_month: Month
622 label_month: Month
622 label_week: Week
623 label_week: Week
623 label_date_from: From
624 label_date_from: From
624 label_date_to: To
625 label_date_to: To
625 label_language_based: Based on user's language
626 label_language_based: Based on user's language
626 label_sort_by: "Sort by {{value}}"
627 label_sort_by: "Sort by {{value}}"
627 label_send_test_email: Send a test email
628 label_send_test_email: Send a test email
628 label_feeds_access_key_created_on: "RSS access key created {{value}} ago"
629 label_feeds_access_key_created_on: "RSS access key created {{value}} ago"
629 label_module_plural: Modules
630 label_module_plural: Modules
630 label_added_time_by: "Added by {{author}} {{age}} ago"
631 label_added_time_by: "Added by {{author}} {{age}} ago"
631 label_updated_time_by: "Updated by {{author}} {{age}} ago"
632 label_updated_time_by: "Updated by {{author}} {{age}} ago"
632 label_updated_time: "Updated {{value}} ago"
633 label_updated_time: "Updated {{value}} ago"
633 label_jump_to_a_project: Jump to a project...
634 label_jump_to_a_project: Jump to a project...
634 label_file_plural: Files
635 label_file_plural: Files
635 label_changeset_plural: Changesets
636 label_changeset_plural: Changesets
636 label_default_columns: Default columns
637 label_default_columns: Default columns
637 label_no_change_option: (No change)
638 label_no_change_option: (No change)
638 label_bulk_edit_selected_issues: Bulk edit selected issues
639 label_bulk_edit_selected_issues: Bulk edit selected issues
639 label_theme: Theme
640 label_theme: Theme
640 label_default: Default
641 label_default: Default
641 label_search_titles_only: Search titles only
642 label_search_titles_only: Search titles only
642 label_user_mail_option_all: "For any event on all my projects"
643 label_user_mail_option_all: "For any event on all my projects"
643 label_user_mail_option_selected: "For any event on the selected projects only..."
644 label_user_mail_option_selected: "For any event on the selected projects only..."
644 label_user_mail_option_none: "Only for things I watch or I'm involved in"
645 label_user_mail_option_none: "Only for things I watch or I'm involved in"
645 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
646 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
646 label_registration_activation_by_email: account activation by email
647 label_registration_activation_by_email: account activation by email
647 label_registration_manual_activation: manual account activation
648 label_registration_manual_activation: manual account activation
648 label_registration_automatic_activation: automatic account activation
649 label_registration_automatic_activation: automatic account activation
649 label_display_per_page: "Per page: {{value}}"
650 label_display_per_page: "Per page: {{value}}"
650 label_age: Age
651 label_age: Age
651 label_change_properties: Change properties
652 label_change_properties: Change properties
652 label_general: General
653 label_general: General
653 label_more: More
654 label_more: More
654 label_scm: SCM
655 label_scm: SCM
655 label_plugins: Plugins
656 label_plugins: Plugins
656 label_ldap_authentication: LDAP authentication
657 label_ldap_authentication: LDAP authentication
657 label_downloads_abbr: D/L
658 label_downloads_abbr: D/L
658 label_optional_description: Optional description
659 label_optional_description: Optional description
659 label_add_another_file: Add another file
660 label_add_another_file: Add another file
660 label_preferences: Preferences
661 label_preferences: Preferences
661 label_chronological_order: In chronological order
662 label_chronological_order: In chronological order
662 label_reverse_chronological_order: In reverse chronological order
663 label_reverse_chronological_order: In reverse chronological order
663 label_planning: Planning
664 label_planning: Planning
664 label_incoming_emails: Incoming emails
665 label_incoming_emails: Incoming emails
665 label_generate_key: Generate a key
666 label_generate_key: Generate a key
666 label_issue_watchers: Watchers
667 label_issue_watchers: Watchers
667 label_example: Example
668 label_example: Example
668 label_display: Display
669 label_display: Display
669 label_sort: Sort
670 label_sort: Sort
670 label_ascending: Ascending
671 label_ascending: Ascending
671 label_descending: Descending
672 label_descending: Descending
672 label_date_from_to: From {{start}} to {{end}}
673 label_date_from_to: From {{start}} to {{end}}
673
674
674 button_login: Login
675 button_login: Login
675 button_submit: Submit
676 button_submit: Submit
676 button_save: Save
677 button_save: Save
677 button_check_all: Check all
678 button_check_all: Check all
678 button_uncheck_all: Uncheck all
679 button_uncheck_all: Uncheck all
679 button_delete: Delete
680 button_delete: Delete
680 button_create: Create
681 button_create: Create
681 button_create_and_continue: Create and continue
682 button_create_and_continue: Create and continue
682 button_test: Test
683 button_test: Test
683 button_edit: Edit
684 button_edit: Edit
684 button_add: Add
685 button_add: Add
685 button_change: Change
686 button_change: Change
686 button_apply: Apply
687 button_apply: Apply
687 button_clear: Clear
688 button_clear: Clear
688 button_lock: Lock
689 button_lock: Lock
689 button_unlock: Unlock
690 button_unlock: Unlock
690 button_download: Download
691 button_download: Download
691 button_list: List
692 button_list: List
692 button_view: View
693 button_view: View
693 button_move: Move
694 button_move: Move
694 button_back: Back
695 button_back: Back
695 button_cancel: Cancel
696 button_cancel: Cancel
696 button_activate: Activate
697 button_activate: Activate
697 button_sort: Sort
698 button_sort: Sort
698 button_log_time: Log time
699 button_log_time: Log time
699 button_rollback: Rollback to this version
700 button_rollback: Rollback to this version
700 button_watch: Watch
701 button_watch: Watch
701 button_unwatch: Unwatch
702 button_unwatch: Unwatch
702 button_reply: Reply
703 button_reply: Reply
703 button_archive: Archive
704 button_archive: Archive
704 button_unarchive: Unarchive
705 button_unarchive: Unarchive
705 button_reset: Reset
706 button_reset: Reset
706 button_rename: Rename
707 button_rename: Rename
707 button_change_password: Change password
708 button_change_password: Change password
708 button_copy: Copy
709 button_copy: Copy
709 button_annotate: Annotate
710 button_annotate: Annotate
710 button_update: Update
711 button_update: Update
711 button_configure: Configure
712 button_configure: Configure
712 button_quote: Quote
713 button_quote: Quote
713
714
714 status_active: active
715 status_active: active
715 status_registered: registered
716 status_registered: registered
716 status_locked: locked
717 status_locked: locked
717
718
718 text_select_mail_notifications: Select actions for which email notifications should be sent.
719 text_select_mail_notifications: Select actions for which email notifications should be sent.
719 text_regexp_info: eg. ^[A-Z0-9]+$
720 text_regexp_info: eg. ^[A-Z0-9]+$
720 text_min_max_length_info: 0 means no restriction
721 text_min_max_length_info: 0 means no restriction
721 text_project_destroy_confirmation: Are you sure you want to delete this project and related data ?
722 text_project_destroy_confirmation: Are you sure you want to delete this project and related data ?
722 text_subprojects_destroy_warning: "Its subproject(s): {{value}} will be also deleted."
723 text_subprojects_destroy_warning: "Its subproject(s): {{value}} will be also deleted."
723 text_workflow_edit: Select a role and a tracker to edit the workflow
724 text_workflow_edit: Select a role and a tracker to edit the workflow
724 text_are_you_sure: Are you sure ?
725 text_are_you_sure: Are you sure ?
725 text_journal_changed: "changed from {{old}} to {{new}}"
726 text_journal_changed: "changed from {{old}} to {{new}}"
726 text_journal_set_to: "set to {{value}}"
727 text_journal_set_to: "set to {{value}}"
727 text_journal_deleted: deleted
728 text_journal_deleted: deleted
728 text_tip_task_begin_day: task beginning this day
729 text_tip_task_begin_day: task beginning this day
729 text_tip_task_end_day: task ending this day
730 text_tip_task_end_day: task ending this day
730 text_tip_task_begin_end_day: task beginning and ending this day
731 text_tip_task_begin_end_day: task beginning and ending this day
731 text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.<br />Once saved, the identifier can not be changed.'
732 text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.<br />Once saved, the identifier can not be changed.'
732 text_caracters_maximum: "{{count}} characters maximum."
733 text_caracters_maximum: "{{count}} characters maximum."
733 text_caracters_minimum: "Must be at least {{count}} characters long."
734 text_caracters_minimum: "Must be at least {{count}} characters long."
734 text_length_between: "Length between {{min}} and {{max}} characters."
735 text_length_between: "Length between {{min}} and {{max}} characters."
735 text_tracker_no_workflow: No workflow defined for this tracker
736 text_tracker_no_workflow: No workflow defined for this tracker
736 text_unallowed_characters: Unallowed characters
737 text_unallowed_characters: Unallowed characters
737 text_comma_separated: Multiple values allowed (comma separated).
738 text_comma_separated: Multiple values allowed (comma separated).
738 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
739 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
739 text_issue_added: "Issue {{id}} has been reported by {{author}}."
740 text_issue_added: "Issue {{id}} has been reported by {{author}}."
740 text_issue_updated: "Issue {{id}} has been updated by {{author}}."
741 text_issue_updated: "Issue {{id}} has been updated by {{author}}."
741 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ?
742 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ?
742 text_issue_category_destroy_question: "Some issues ({{count}}) are assigned to this category. What do you want to do ?"
743 text_issue_category_destroy_question: "Some issues ({{count}}) are assigned to this category. What do you want to do ?"
743 text_issue_category_destroy_assignments: Remove category assignments
744 text_issue_category_destroy_assignments: Remove category assignments
744 text_issue_category_reassign_to: Reassign issues to this category
745 text_issue_category_reassign_to: Reassign issues to this category
745 text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
746 text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
746 text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
747 text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
747 text_load_default_configuration: Load the default configuration
748 text_load_default_configuration: Load the default configuration
748 text_status_changed_by_changeset: "Applied in changeset {{value}}."
749 text_status_changed_by_changeset: "Applied in changeset {{value}}."
749 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
750 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
750 text_select_project_modules: 'Select modules to enable for this project:'
751 text_select_project_modules: 'Select modules to enable for this project:'
751 text_default_administrator_account_changed: Default administrator account changed
752 text_default_administrator_account_changed: Default administrator account changed
752 text_file_repository_writable: Attachments directory writable
753 text_file_repository_writable: Attachments directory writable
753 text_plugin_assets_writable: Plugin assets directory writable
754 text_plugin_assets_writable: Plugin assets directory writable
754 text_rmagick_available: RMagick available (optional)
755 text_rmagick_available: RMagick available (optional)
755 text_destroy_time_entries_question: "{{hours}} hours were reported on the issues you are about to delete. What do you want to do ?"
756 text_destroy_time_entries_question: "{{hours}} hours were reported on the issues you are about to delete. What do you want to do ?"
756 text_destroy_time_entries: Delete reported hours
757 text_destroy_time_entries: Delete reported hours
757 text_assign_time_entries_to_project: Assign reported hours to the project
758 text_assign_time_entries_to_project: Assign reported hours to the project
758 text_reassign_time_entries: 'Reassign reported hours to this issue:'
759 text_reassign_time_entries: 'Reassign reported hours to this issue:'
759 text_user_wrote: "{{value}} wrote:"
760 text_user_wrote: "{{value}} wrote:"
760 text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
761 text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
761 text_enumeration_category_reassign_to: 'Reassign them to this value:'
762 text_enumeration_category_reassign_to: 'Reassign them to this value:'
762 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
763 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
763 text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
764 text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
764 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
765 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
765 text_custom_field_possible_values_info: 'One line for each value'
766 text_custom_field_possible_values_info: 'One line for each value'
766 text_wiki_page_destroy_question: "This page has {{descendants}} child page(s) and descendant(s). What do you want to do?"
767 text_wiki_page_destroy_question: "This page has {{descendants}} child page(s) and descendant(s). What do you want to do?"
767 text_wiki_page_nullify_children: "Keep child pages as root pages"
768 text_wiki_page_nullify_children: "Keep child pages as root pages"
768 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
769 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
769 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
770 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
770
771
771 default_role_manager: Manager
772 default_role_manager: Manager
772 default_role_developper: Developer
773 default_role_developper: Developer
773 default_role_reporter: Reporter
774 default_role_reporter: Reporter
774 default_tracker_bug: Bug
775 default_tracker_bug: Bug
775 default_tracker_feature: Feature
776 default_tracker_feature: Feature
776 default_tracker_support: Support
777 default_tracker_support: Support
777 default_issue_status_new: New
778 default_issue_status_new: New
778 default_issue_status_assigned: Assigned
779 default_issue_status_assigned: Assigned
779 default_issue_status_resolved: Resolved
780 default_issue_status_resolved: Resolved
780 default_issue_status_feedback: Feedback
781 default_issue_status_feedback: Feedback
781 default_issue_status_closed: Closed
782 default_issue_status_closed: Closed
782 default_issue_status_rejected: Rejected
783 default_issue_status_rejected: Rejected
783 default_doc_category_user: User documentation
784 default_doc_category_user: User documentation
784 default_doc_category_tech: Technical documentation
785 default_doc_category_tech: Technical documentation
785 default_priority_low: Low
786 default_priority_low: Low
786 default_priority_normal: Normal
787 default_priority_normal: Normal
787 default_priority_high: High
788 default_priority_high: High
788 default_priority_urgent: Urgent
789 default_priority_urgent: Urgent
789 default_priority_immediate: Immediate
790 default_priority_immediate: Immediate
790 default_activity_design: Design
791 default_activity_design: Design
791 default_activity_development: Development
792 default_activity_development: Development
792
793
793 enumeration_issue_priorities: Issue priorities
794 enumeration_issue_priorities: Issue priorities
794 enumeration_doc_categories: Document categories
795 enumeration_doc_categories: Document categories
795 enumeration_activities: Activities (time tracking)
796 enumeration_activities: Activities (time tracking)
@@ -1,128 +1,128
1 ---
1 ---
2 issues_001:
2 issues_001:
3 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
3 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
4 project_id: 1
4 project_id: 1
5 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
5 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
6 priority_id: 4
6 priority_id: 4
7 subject: Can't print recipes
7 subject: Can't print recipes
8 id: 1
8 id: 1
9 fixed_version_id:
9 fixed_version_id:
10 category_id: 1
10 category_id: 1
11 description: Unable to print recipes
11 description: Unable to print recipes
12 tracker_id: 1
12 tracker_id: 1
13 assigned_to_id:
13 assigned_to_id:
14 author_id: 2
14 author_id: 2
15 status_id: 1
15 status_id: 1
16 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
16 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
17 due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
17 due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
18 issues_002:
18 issues_002:
19 created_on: 2006-07-19 21:04:21 +02:00
19 created_on: 2006-07-19 21:04:21 +02:00
20 project_id: 1
20 project_id: 1
21 updated_on: 2006-07-19 21:09:50 +02:00
21 updated_on: 2006-07-19 21:09:50 +02:00
22 priority_id: 5
22 priority_id: 5
23 subject: Add ingredients categories
23 subject: Add ingredients categories
24 id: 2
24 id: 2
25 fixed_version_id: 2
25 fixed_version_id: 2
26 category_id:
26 category_id:
27 description: Ingredients of the recipe should be classified by categories
27 description: Ingredients of the recipe should be classified by categories
28 tracker_id: 2
28 tracker_id: 2
29 assigned_to_id: 3
29 assigned_to_id: 3
30 author_id: 2
30 author_id: 2
31 status_id: 2
31 status_id: 2
32 start_date: <%= 2.day.ago.to_date.to_s(:db) %>
32 start_date: <%= 2.day.ago.to_date.to_s(:db) %>
33 due_date:
33 due_date:
34 issues_003:
34 issues_003:
35 created_on: 2006-07-19 21:07:27 +02:00
35 created_on: 2006-07-19 21:07:27 +02:00
36 project_id: 1
36 project_id: 1
37 updated_on: 2006-07-19 21:07:27 +02:00
37 updated_on: 2006-07-19 21:07:27 +02:00
38 priority_id: 4
38 priority_id: 4
39 subject: Error 281 when updating a recipe
39 subject: Error 281 when updating a recipe
40 id: 3
40 id: 3
41 fixed_version_id:
41 fixed_version_id:
42 category_id:
42 category_id:
43 description: Error 281 is encountered when saving a recipe
43 description: Error 281 is encountered when saving a recipe
44 tracker_id: 1
44 tracker_id: 1
45 assigned_to_id: 3
45 assigned_to_id: 3
46 author_id: 2
46 author_id: 2
47 status_id: 1
47 status_id: 1
48 start_date: <%= 1.day.from_now.to_date.to_s(:db) %>
48 start_date: <%= 1.day.from_now.to_date.to_s(:db) %>
49 due_date: <%= 40.day.ago.to_date.to_s(:db) %>
49 due_date: <%= 40.day.ago.to_date.to_s(:db) %>
50 issues_004:
50 issues_004:
51 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
51 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
52 project_id: 2
52 project_id: 2
53 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
53 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
54 priority_id: 4
54 priority_id: 4
55 subject: Issue on project 2
55 subject: Issue on project 2
56 id: 4
56 id: 4
57 fixed_version_id:
57 fixed_version_id:
58 category_id:
58 category_id:
59 description: Issue on project 2
59 description: Issue on project 2
60 tracker_id: 1
60 tracker_id: 1
61 assigned_to_id:
61 assigned_to_id: 2
62 author_id: 2
62 author_id: 2
63 status_id: 1
63 status_id: 1
64 issues_005:
64 issues_005:
65 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
65 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
66 project_id: 3
66 project_id: 3
67 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
67 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
68 priority_id: 4
68 priority_id: 4
69 subject: Subproject issue
69 subject: Subproject issue
70 id: 5
70 id: 5
71 fixed_version_id:
71 fixed_version_id:
72 category_id:
72 category_id:
73 description: This is an issue on a cookbook subproject
73 description: This is an issue on a cookbook subproject
74 tracker_id: 1
74 tracker_id: 1
75 assigned_to_id:
75 assigned_to_id:
76 author_id: 2
76 author_id: 2
77 status_id: 1
77 status_id: 1
78 issues_006:
78 issues_006:
79 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
79 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
80 project_id: 5
80 project_id: 5
81 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
81 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
82 priority_id: 4
82 priority_id: 4
83 subject: Issue of a private subproject
83 subject: Issue of a private subproject
84 id: 6
84 id: 6
85 fixed_version_id:
85 fixed_version_id:
86 category_id:
86 category_id:
87 description: This is an issue of a private subproject of cookbook
87 description: This is an issue of a private subproject of cookbook
88 tracker_id: 1
88 tracker_id: 1
89 assigned_to_id:
89 assigned_to_id:
90 author_id: 2
90 author_id: 2
91 status_id: 1
91 status_id: 1
92 start_date: <%= Date.today.to_s(:db) %>
92 start_date: <%= Date.today.to_s(:db) %>
93 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
93 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
94 issues_007:
94 issues_007:
95 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
95 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
96 project_id: 1
96 project_id: 1
97 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
97 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
98 priority_id: 3
98 priority_id: 3
99 subject: Issue due today
99 subject: Issue due today
100 id: 7
100 id: 7
101 fixed_version_id:
101 fixed_version_id:
102 category_id:
102 category_id:
103 description: This is an issue that is due today
103 description: This is an issue that is due today
104 tracker_id: 1
104 tracker_id: 1
105 assigned_to_id:
105 assigned_to_id:
106 author_id: 2
106 author_id: 2
107 status_id: 1
107 status_id: 1
108 start_date: <%= 10.days.ago.to_s(:db) %>
108 start_date: <%= 10.days.ago.to_s(:db) %>
109 due_date: <%= Date.today.to_s(:db) %>
109 due_date: <%= Date.today.to_s(:db) %>
110 lock_version: 0
110 lock_version: 0
111 issues_008:
111 issues_008:
112 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
112 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
113 project_id: 1
113 project_id: 1
114 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
114 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
115 priority_id: 3
115 priority_id: 3
116 subject: Closed issue
116 subject: Closed issue
117 id: 8
117 id: 8
118 fixed_version_id:
118 fixed_version_id:
119 category_id:
119 category_id:
120 description: This is a closed issue.
120 description: This is a closed issue.
121 tracker_id: 1
121 tracker_id: 1
122 assigned_to_id:
122 assigned_to_id:
123 author_id: 2
123 author_id: 2
124 status_id: 5
124 status_id: 5
125 start_date:
125 start_date:
126 due_date:
126 due_date:
127 lock_version: 0
127 lock_version: 0
128 No newline at end of file
128
@@ -1,109 +1,137
1 ---
1 ---
2 queries_001:
2 queries_001:
3 id: 1
3 id: 1
4 project_id: 1
4 project_id: 1
5 is_public: true
5 is_public: true
6 name: Multiple custom fields query
6 name: Multiple custom fields query
7 filters: |
7 filters: |
8 ---
8 ---
9 cf_1:
9 cf_1:
10 :values:
10 :values:
11 - MySQL
11 - MySQL
12 :operator: "="
12 :operator: "="
13 status_id:
13 status_id:
14 :values:
14 :values:
15 - "1"
15 - "1"
16 :operator: o
16 :operator: o
17 cf_2:
17 cf_2:
18 :values:
18 :values:
19 - "125"
19 - "125"
20 :operator: "="
20 :operator: "="
21
21
22 user_id: 1
22 user_id: 1
23 column_names:
23 column_names:
24 queries_002:
24 queries_002:
25 id: 2
25 id: 2
26 project_id: 1
26 project_id: 1
27 is_public: false
27 is_public: false
28 name: Private query for cookbook
28 name: Private query for cookbook
29 filters: |
29 filters: |
30 ---
30 ---
31 tracker_id:
31 tracker_id:
32 :values:
32 :values:
33 - "3"
33 - "3"
34 :operator: "="
34 :operator: "="
35 status_id:
35 status_id:
36 :values:
36 :values:
37 - "1"
37 - "1"
38 :operator: o
38 :operator: o
39
39
40 user_id: 3
40 user_id: 3
41 column_names:
41 column_names:
42 queries_003:
42 queries_003:
43 id: 3
43 id: 3
44 project_id:
44 project_id:
45 is_public: false
45 is_public: false
46 name: Private query for all projects
46 name: Private query for all projects
47 filters: |
47 filters: |
48 ---
48 ---
49 tracker_id:
49 tracker_id:
50 :values:
50 :values:
51 - "3"
51 - "3"
52 :operator: "="
52 :operator: "="
53
53
54 user_id: 3
54 user_id: 3
55 column_names:
55 column_names:
56 queries_004:
56 queries_004:
57 id: 4
57 id: 4
58 project_id:
58 project_id:
59 is_public: true
59 is_public: true
60 name: Public query for all projects
60 name: Public query for all projects
61 filters: |
61 filters: |
62 ---
62 ---
63 tracker_id:
63 tracker_id:
64 :values:
64 :values:
65 - "3"
65 - "3"
66 :operator: "="
66 :operator: "="
67
67
68 user_id: 2
68 user_id: 2
69 column_names:
69 column_names:
70 queries_005:
70 queries_005:
71 id: 5
71 id: 5
72 project_id:
72 project_id:
73 is_public: true
73 is_public: true
74 name: Open issues by priority and tracker
74 name: Open issues by priority and tracker
75 filters: |
75 filters: |
76 ---
76 ---
77 status_id:
77 status_id:
78 :values:
78 :values:
79 - "1"
79 - "1"
80 :operator: o
80 :operator: o
81
81
82 user_id: 1
82 user_id: 1
83 column_names:
83 column_names:
84 sort_criteria: |
84 sort_criteria: |
85 ---
85 ---
86 - - priority
86 - - priority
87 - desc
87 - desc
88 - - tracker
88 - - tracker
89 - asc
89 - asc
90 queries_006:
90 queries_006:
91 id: 6
91 id: 6
92 project_id:
92 project_id:
93 is_public: true
93 is_public: true
94 name: Open issues grouped by tracker
94 name: Open issues grouped by tracker
95 filters: |
95 filters: |
96 ---
96 ---
97 status_id:
97 status_id:
98 :values:
98 :values:
99 - "1"
99 - "1"
100 :operator: o
100 :operator: o
101
101
102 user_id: 1
102 user_id: 1
103 column_names:
103 column_names:
104 group_by: tracker
104 group_by: tracker
105 sort_criteria: |
105 sort_criteria: |
106 ---
106 ---
107 - - priority
107 - - priority
108 - desc
108 - desc
109 No newline at end of file
109 queries_007:
110 id: 7
111 project_id: 2
112 is_public: true
113 name: Public query for project 2
114 filters: |
115 ---
116 tracker_id:
117 :values:
118 - "3"
119 :operator: "="
120
121 user_id: 2
122 column_names:
123 queries_008:
124 id: 8
125 project_id: 2
126 is_public: false
127 name: Private query for project 2
128 filters: |
129 ---
130 tracker_id:
131 :values:
132 - "3"
133 :operator: "="
134
135 user_id: 2
136 column_names:
137
@@ -1,501 +1,517
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'projects_controller'
19 require 'projects_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class ProjectsController; def rescue_action(e) raise e end; end
22 class ProjectsController; def rescue_action(e) raise e end; end
23
23
24 class ProjectsControllerTest < Test::Unit::TestCase
24 class ProjectsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details,
25 fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
27 :attachments
27 :attachments
28
28
29 def setup
29 def setup
30 @controller = ProjectsController.new
30 @controller = ProjectsController.new
31 @request = ActionController::TestRequest.new
31 @request = ActionController::TestRequest.new
32 @response = ActionController::TestResponse.new
32 @response = ActionController::TestResponse.new
33 @request.session[:user_id] = nil
33 @request.session[:user_id] = nil
34 Setting.default_language = 'en'
34 Setting.default_language = 'en'
35 end
35 end
36
36
37 def test_index_routing
37 def test_index_routing
38 assert_routing(
38 assert_routing(
39 {:method => :get, :path => '/projects'},
39 {:method => :get, :path => '/projects'},
40 :controller => 'projects', :action => 'index'
40 :controller => 'projects', :action => 'index'
41 )
41 )
42 end
42 end
43
43
44 def test_index
44 def test_index
45 get :index
45 get :index
46 assert_response :success
46 assert_response :success
47 assert_template 'index'
47 assert_template 'index'
48 assert_not_nil assigns(:projects)
48 assert_not_nil assigns(:projects)
49
49
50 assert_tag :ul, :child => {:tag => 'li',
50 assert_tag :ul, :child => {:tag => 'li',
51 :descendant => {:tag => 'a', :content => 'eCookbook'},
51 :descendant => {:tag => 'a', :content => 'eCookbook'},
52 :child => { :tag => 'ul',
52 :child => { :tag => 'ul',
53 :descendant => { :tag => 'a',
53 :descendant => { :tag => 'a',
54 :content => 'Child of private child'
54 :content => 'Child of private child'
55 }
55 }
56 }
56 }
57 }
57 }
58
58
59 assert_no_tag :a, :content => /Private child of eCookbook/
59 assert_no_tag :a, :content => /Private child of eCookbook/
60 end
60 end
61
61
62 def test_index_atom_routing
62 def test_index_atom_routing
63 assert_routing(
63 assert_routing(
64 {:method => :get, :path => '/projects.atom'},
64 {:method => :get, :path => '/projects.atom'},
65 :controller => 'projects', :action => 'index', :format => 'atom'
65 :controller => 'projects', :action => 'index', :format => 'atom'
66 )
66 )
67 end
67 end
68
68
69 def test_index_atom
69 def test_index_atom
70 get :index, :format => 'atom'
70 get :index, :format => 'atom'
71 assert_response :success
71 assert_response :success
72 assert_template 'common/feed.atom.rxml'
72 assert_template 'common/feed.atom.rxml'
73 assert_select 'feed>title', :text => 'Redmine: Latest projects'
73 assert_select 'feed>title', :text => 'Redmine: Latest projects'
74 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
74 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
75 end
75 end
76
76
77 def test_add_routing
77 def test_add_routing
78 assert_routing(
78 assert_routing(
79 {:method => :get, :path => '/projects/new'},
79 {:method => :get, :path => '/projects/new'},
80 :controller => 'projects', :action => 'add'
80 :controller => 'projects', :action => 'add'
81 )
81 )
82 assert_recognizes(
82 assert_recognizes(
83 {:controller => 'projects', :action => 'add'},
83 {:controller => 'projects', :action => 'add'},
84 {:method => :post, :path => '/projects/new'}
84 {:method => :post, :path => '/projects/new'}
85 )
85 )
86 assert_recognizes(
86 assert_recognizes(
87 {:controller => 'projects', :action => 'add'},
87 {:controller => 'projects', :action => 'add'},
88 {:method => :post, :path => '/projects'}
88 {:method => :post, :path => '/projects'}
89 )
89 )
90 end
90 end
91
91
92 def test_show_routing
92 def test_show_routing
93 assert_routing(
93 assert_routing(
94 {:method => :get, :path => '/projects/test'},
94 {:method => :get, :path => '/projects/test'},
95 :controller => 'projects', :action => 'show', :id => 'test'
95 :controller => 'projects', :action => 'show', :id => 'test'
96 )
96 )
97 end
97 end
98
98
99 def test_show_by_id
99 def test_show_by_id
100 get :show, :id => 1
100 get :show, :id => 1
101 assert_response :success
101 assert_response :success
102 assert_template 'show'
102 assert_template 'show'
103 assert_not_nil assigns(:project)
103 assert_not_nil assigns(:project)
104 end
104 end
105
105
106 def test_show_by_identifier
106 def test_show_by_identifier
107 get :show, :id => 'ecookbook'
107 get :show, :id => 'ecookbook'
108 assert_response :success
108 assert_response :success
109 assert_template 'show'
109 assert_template 'show'
110 assert_not_nil assigns(:project)
110 assert_not_nil assigns(:project)
111 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
111 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
112 end
112 end
113
113
114 def test_private_subprojects_hidden
114 def test_private_subprojects_hidden
115 get :show, :id => 'ecookbook'
115 get :show, :id => 'ecookbook'
116 assert_response :success
116 assert_response :success
117 assert_template 'show'
117 assert_template 'show'
118 assert_no_tag :tag => 'a', :content => /Private child/
118 assert_no_tag :tag => 'a', :content => /Private child/
119 end
119 end
120
120
121 def test_private_subprojects_visible
121 def test_private_subprojects_visible
122 @request.session[:user_id] = 2 # manager who is a member of the private subproject
122 @request.session[:user_id] = 2 # manager who is a member of the private subproject
123 get :show, :id => 'ecookbook'
123 get :show, :id => 'ecookbook'
124 assert_response :success
124 assert_response :success
125 assert_template 'show'
125 assert_template 'show'
126 assert_tag :tag => 'a', :content => /Private child/
126 assert_tag :tag => 'a', :content => /Private child/
127 end
127 end
128
128
129 def test_settings_routing
129 def test_settings_routing
130 assert_routing(
130 assert_routing(
131 {:method => :get, :path => '/projects/4223/settings'},
131 {:method => :get, :path => '/projects/4223/settings'},
132 :controller => 'projects', :action => 'settings', :id => '4223'
132 :controller => 'projects', :action => 'settings', :id => '4223'
133 )
133 )
134 assert_routing(
134 assert_routing(
135 {:method => :get, :path => '/projects/4223/settings/members'},
135 {:method => :get, :path => '/projects/4223/settings/members'},
136 :controller => 'projects', :action => 'settings', :id => '4223', :tab => 'members'
136 :controller => 'projects', :action => 'settings', :id => '4223', :tab => 'members'
137 )
137 )
138 end
138 end
139
139
140 def test_settings
140 def test_settings
141 @request.session[:user_id] = 2 # manager
141 @request.session[:user_id] = 2 # manager
142 get :settings, :id => 1
142 get :settings, :id => 1
143 assert_response :success
143 assert_response :success
144 assert_template 'settings'
144 assert_template 'settings'
145 end
145 end
146
146
147 def test_edit
147 def test_edit
148 @request.session[:user_id] = 2 # manager
148 @request.session[:user_id] = 2 # manager
149 post :edit, :id => 1, :project => {:name => 'Test changed name',
149 post :edit, :id => 1, :project => {:name => 'Test changed name',
150 :issue_custom_field_ids => ['']}
150 :issue_custom_field_ids => ['']}
151 assert_redirected_to 'projects/ecookbook/settings'
151 assert_redirected_to 'projects/ecookbook/settings'
152 project = Project.find(1)
152 project = Project.find(1)
153 assert_equal 'Test changed name', project.name
153 assert_equal 'Test changed name', project.name
154 end
154 end
155
155
156 def test_add_version_routing
156 def test_add_version_routing
157 assert_routing(
157 assert_routing(
158 {:method => :get, :path => 'projects/64/versions/new'},
158 {:method => :get, :path => 'projects/64/versions/new'},
159 :controller => 'projects', :action => 'add_version', :id => '64'
159 :controller => 'projects', :action => 'add_version', :id => '64'
160 )
160 )
161 assert_routing(
161 assert_routing(
162 #TODO: use PUT
162 #TODO: use PUT
163 {:method => :post, :path => 'projects/64/versions/new'},
163 {:method => :post, :path => 'projects/64/versions/new'},
164 :controller => 'projects', :action => 'add_version', :id => '64'
164 :controller => 'projects', :action => 'add_version', :id => '64'
165 )
165 )
166 end
166 end
167
167
168 def test_add_issue_category_routing
168 def test_add_issue_category_routing
169 assert_routing(
169 assert_routing(
170 {:method => :get, :path => 'projects/test/categories/new'},
170 {:method => :get, :path => 'projects/test/categories/new'},
171 :controller => 'projects', :action => 'add_issue_category', :id => 'test'
171 :controller => 'projects', :action => 'add_issue_category', :id => 'test'
172 )
172 )
173 assert_routing(
173 assert_routing(
174 #TODO: use PUT and update form
174 #TODO: use PUT and update form
175 {:method => :post, :path => 'projects/64/categories/new'},
175 {:method => :post, :path => 'projects/64/categories/new'},
176 :controller => 'projects', :action => 'add_issue_category', :id => '64'
176 :controller => 'projects', :action => 'add_issue_category', :id => '64'
177 )
177 )
178 end
178 end
179
179
180 def test_destroy_routing
180 def test_destroy_routing
181 assert_routing(
181 assert_routing(
182 {:method => :get, :path => '/projects/567/destroy'},
182 {:method => :get, :path => '/projects/567/destroy'},
183 :controller => 'projects', :action => 'destroy', :id => '567'
183 :controller => 'projects', :action => 'destroy', :id => '567'
184 )
184 )
185 assert_routing(
185 assert_routing(
186 #TODO: use DELETE and update form
186 #TODO: use DELETE and update form
187 {:method => :post, :path => 'projects/64/destroy'},
187 {:method => :post, :path => 'projects/64/destroy'},
188 :controller => 'projects', :action => 'destroy', :id => '64'
188 :controller => 'projects', :action => 'destroy', :id => '64'
189 )
189 )
190 end
190 end
191
191
192 def test_get_destroy
192 def test_get_destroy
193 @request.session[:user_id] = 1 # admin
193 @request.session[:user_id] = 1 # admin
194 get :destroy, :id => 1
194 get :destroy, :id => 1
195 assert_response :success
195 assert_response :success
196 assert_template 'destroy'
196 assert_template 'destroy'
197 assert_not_nil Project.find_by_id(1)
197 assert_not_nil Project.find_by_id(1)
198 end
198 end
199
199
200 def test_post_destroy
200 def test_post_destroy
201 @request.session[:user_id] = 1 # admin
201 @request.session[:user_id] = 1 # admin
202 post :destroy, :id => 1, :confirm => 1
202 post :destroy, :id => 1, :confirm => 1
203 assert_redirected_to 'admin/projects'
203 assert_redirected_to 'admin/projects'
204 assert_nil Project.find_by_id(1)
204 assert_nil Project.find_by_id(1)
205 end
205 end
206
206
207 def test_add_file
207 def test_add_file
208 set_tmp_attachments_directory
208 set_tmp_attachments_directory
209 @request.session[:user_id] = 2
209 @request.session[:user_id] = 2
210 Setting.notified_events = ['file_added']
210 Setting.notified_events = ['file_added']
211 ActionMailer::Base.deliveries.clear
211 ActionMailer::Base.deliveries.clear
212
212
213 assert_difference 'Attachment.count' do
213 assert_difference 'Attachment.count' do
214 post :add_file, :id => 1, :version_id => '',
214 post :add_file, :id => 1, :version_id => '',
215 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
215 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
216 end
216 end
217 assert_redirected_to 'projects/ecookbook/files'
217 assert_redirected_to 'projects/ecookbook/files'
218 a = Attachment.find(:first, :order => 'created_on DESC')
218 a = Attachment.find(:first, :order => 'created_on DESC')
219 assert_equal 'testfile.txt', a.filename
219 assert_equal 'testfile.txt', a.filename
220 assert_equal Project.find(1), a.container
220 assert_equal Project.find(1), a.container
221
221
222 mail = ActionMailer::Base.deliveries.last
222 mail = ActionMailer::Base.deliveries.last
223 assert_kind_of TMail::Mail, mail
223 assert_kind_of TMail::Mail, mail
224 assert_equal "[eCookbook] New file", mail.subject
224 assert_equal "[eCookbook] New file", mail.subject
225 assert mail.body.include?('testfile.txt')
225 assert mail.body.include?('testfile.txt')
226 end
226 end
227
227
228 def test_add_file_routing
228 def test_add_file_routing
229 assert_routing(
229 assert_routing(
230 {:method => :get, :path => '/projects/33/files/new'},
230 {:method => :get, :path => '/projects/33/files/new'},
231 :controller => 'projects', :action => 'add_file', :id => '33'
231 :controller => 'projects', :action => 'add_file', :id => '33'
232 )
232 )
233 assert_routing(
233 assert_routing(
234 {:method => :post, :path => '/projects/33/files/new'},
234 {:method => :post, :path => '/projects/33/files/new'},
235 :controller => 'projects', :action => 'add_file', :id => '33'
235 :controller => 'projects', :action => 'add_file', :id => '33'
236 )
236 )
237 end
237 end
238
238
239 def test_add_version_file
239 def test_add_version_file
240 set_tmp_attachments_directory
240 set_tmp_attachments_directory
241 @request.session[:user_id] = 2
241 @request.session[:user_id] = 2
242 Setting.notified_events = ['file_added']
242 Setting.notified_events = ['file_added']
243
243
244 assert_difference 'Attachment.count' do
244 assert_difference 'Attachment.count' do
245 post :add_file, :id => 1, :version_id => '2',
245 post :add_file, :id => 1, :version_id => '2',
246 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
246 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
247 end
247 end
248 assert_redirected_to 'projects/ecookbook/files'
248 assert_redirected_to 'projects/ecookbook/files'
249 a = Attachment.find(:first, :order => 'created_on DESC')
249 a = Attachment.find(:first, :order => 'created_on DESC')
250 assert_equal 'testfile.txt', a.filename
250 assert_equal 'testfile.txt', a.filename
251 assert_equal Version.find(2), a.container
251 assert_equal Version.find(2), a.container
252 end
252 end
253
253
254 def test_list_files
254 def test_list_files
255 get :list_files, :id => 1
255 get :list_files, :id => 1
256 assert_response :success
256 assert_response :success
257 assert_template 'list_files'
257 assert_template 'list_files'
258 assert_not_nil assigns(:containers)
258 assert_not_nil assigns(:containers)
259
259
260 # file attached to the project
260 # file attached to the project
261 assert_tag :a, :content => 'project_file.zip',
261 assert_tag :a, :content => 'project_file.zip',
262 :attributes => { :href => '/attachments/download/8/project_file.zip' }
262 :attributes => { :href => '/attachments/download/8/project_file.zip' }
263
263
264 # file attached to a project's version
264 # file attached to a project's version
265 assert_tag :a, :content => 'version_file.zip',
265 assert_tag :a, :content => 'version_file.zip',
266 :attributes => { :href => '/attachments/download/9/version_file.zip' }
266 :attributes => { :href => '/attachments/download/9/version_file.zip' }
267 end
267 end
268
268
269 def test_list_files_routing
269 def test_list_files_routing
270 assert_routing(
270 assert_routing(
271 {:method => :get, :path => '/projects/33/files'},
271 {:method => :get, :path => '/projects/33/files'},
272 :controller => 'projects', :action => 'list_files', :id => '33'
272 :controller => 'projects', :action => 'list_files', :id => '33'
273 )
273 )
274 end
274 end
275
275
276 def test_changelog_routing
276 def test_changelog_routing
277 assert_routing(
277 assert_routing(
278 {:method => :get, :path => '/projects/44/changelog'},
278 {:method => :get, :path => '/projects/44/changelog'},
279 :controller => 'projects', :action => 'changelog', :id => '44'
279 :controller => 'projects', :action => 'changelog', :id => '44'
280 )
280 )
281 end
281 end
282
282
283 def test_changelog
283 def test_changelog
284 get :changelog, :id => 1
284 get :changelog, :id => 1
285 assert_response :success
285 assert_response :success
286 assert_template 'changelog'
286 assert_template 'changelog'
287 assert_not_nil assigns(:versions)
287 assert_not_nil assigns(:versions)
288 end
288 end
289
289
290 def test_roadmap_routing
290 def test_roadmap_routing
291 assert_routing(
291 assert_routing(
292 {:method => :get, :path => 'projects/33/roadmap'},
292 {:method => :get, :path => 'projects/33/roadmap'},
293 :controller => 'projects', :action => 'roadmap', :id => '33'
293 :controller => 'projects', :action => 'roadmap', :id => '33'
294 )
294 )
295 end
295 end
296
296
297 def test_roadmap
297 def test_roadmap
298 get :roadmap, :id => 1
298 get :roadmap, :id => 1
299 assert_response :success
299 assert_response :success
300 assert_template 'roadmap'
300 assert_template 'roadmap'
301 assert_not_nil assigns(:versions)
301 assert_not_nil assigns(:versions)
302 # Version with no date set appears
302 # Version with no date set appears
303 assert assigns(:versions).include?(Version.find(3))
303 assert assigns(:versions).include?(Version.find(3))
304 # Completed version doesn't appear
304 # Completed version doesn't appear
305 assert !assigns(:versions).include?(Version.find(1))
305 assert !assigns(:versions).include?(Version.find(1))
306 end
306 end
307
307
308 def test_roadmap_with_completed_versions
308 def test_roadmap_with_completed_versions
309 get :roadmap, :id => 1, :completed => 1
309 get :roadmap, :id => 1, :completed => 1
310 assert_response :success
310 assert_response :success
311 assert_template 'roadmap'
311 assert_template 'roadmap'
312 assert_not_nil assigns(:versions)
312 assert_not_nil assigns(:versions)
313 # Version with no date set appears
313 # Version with no date set appears
314 assert assigns(:versions).include?(Version.find(3))
314 assert assigns(:versions).include?(Version.find(3))
315 # Completed version appears
315 # Completed version appears
316 assert assigns(:versions).include?(Version.find(1))
316 assert assigns(:versions).include?(Version.find(1))
317 end
317 end
318
318
319 def test_project_activity_routing
319 def test_project_activity_routing
320 assert_routing(
320 assert_routing(
321 {:method => :get, :path => '/projects/1/activity'},
321 {:method => :get, :path => '/projects/1/activity'},
322 :controller => 'projects', :action => 'activity', :id => '1'
322 :controller => 'projects', :action => 'activity', :id => '1'
323 )
323 )
324 end
324 end
325
325
326 def test_project_activity_atom_routing
326 def test_project_activity_atom_routing
327 assert_routing(
327 assert_routing(
328 {:method => :get, :path => '/projects/1/activity.atom'},
328 {:method => :get, :path => '/projects/1/activity.atom'},
329 :controller => 'projects', :action => 'activity', :id => '1', :format => 'atom'
329 :controller => 'projects', :action => 'activity', :id => '1', :format => 'atom'
330 )
330 )
331 end
331 end
332
332
333 def test_project_activity
333 def test_project_activity
334 get :activity, :id => 1, :with_subprojects => 0
334 get :activity, :id => 1, :with_subprojects => 0
335 assert_response :success
335 assert_response :success
336 assert_template 'activity'
336 assert_template 'activity'
337 assert_not_nil assigns(:events_by_day)
337 assert_not_nil assigns(:events_by_day)
338
338
339 assert_tag :tag => "h3",
339 assert_tag :tag => "h3",
340 :content => /#{2.days.ago.to_date.day}/,
340 :content => /#{2.days.ago.to_date.day}/,
341 :sibling => { :tag => "dl",
341 :sibling => { :tag => "dl",
342 :child => { :tag => "dt",
342 :child => { :tag => "dt",
343 :attributes => { :class => /issue-edit/ },
343 :attributes => { :class => /issue-edit/ },
344 :child => { :tag => "a",
344 :child => { :tag => "a",
345 :content => /(#{IssueStatus.find(2).name})/,
345 :content => /(#{IssueStatus.find(2).name})/,
346 }
346 }
347 }
347 }
348 }
348 }
349 end
349 end
350
350
351 def test_previous_project_activity
351 def test_previous_project_activity
352 get :activity, :id => 1, :from => 3.days.ago.to_date
352 get :activity, :id => 1, :from => 3.days.ago.to_date
353 assert_response :success
353 assert_response :success
354 assert_template 'activity'
354 assert_template 'activity'
355 assert_not_nil assigns(:events_by_day)
355 assert_not_nil assigns(:events_by_day)
356
356
357 assert_tag :tag => "h3",
357 assert_tag :tag => "h3",
358 :content => /#{3.day.ago.to_date.day}/,
358 :content => /#{3.day.ago.to_date.day}/,
359 :sibling => { :tag => "dl",
359 :sibling => { :tag => "dl",
360 :child => { :tag => "dt",
360 :child => { :tag => "dt",
361 :attributes => { :class => /issue/ },
361 :attributes => { :class => /issue/ },
362 :child => { :tag => "a",
362 :child => { :tag => "a",
363 :content => /#{Issue.find(1).subject}/,
363 :content => /#{Issue.find(1).subject}/,
364 }
364 }
365 }
365 }
366 }
366 }
367 end
367 end
368
368
369 def test_global_activity_routing
369 def test_global_activity_routing
370 assert_routing({:method => :get, :path => '/activity'}, :controller => 'projects', :action => 'activity', :id => nil)
370 assert_routing({:method => :get, :path => '/activity'}, :controller => 'projects', :action => 'activity', :id => nil)
371 end
371 end
372
372
373 def test_global_activity
373 def test_global_activity
374 get :activity
374 get :activity
375 assert_response :success
375 assert_response :success
376 assert_template 'activity'
376 assert_template 'activity'
377 assert_not_nil assigns(:events_by_day)
377 assert_not_nil assigns(:events_by_day)
378
378
379 assert_tag :tag => "h3",
379 assert_tag :tag => "h3",
380 :content => /#{5.day.ago.to_date.day}/,
380 :content => /#{5.day.ago.to_date.day}/,
381 :sibling => { :tag => "dl",
381 :sibling => { :tag => "dl",
382 :child => { :tag => "dt",
382 :child => { :tag => "dt",
383 :attributes => { :class => /issue/ },
383 :attributes => { :class => /issue/ },
384 :child => { :tag => "a",
384 :child => { :tag => "a",
385 :content => /#{Issue.find(5).subject}/,
385 :content => /#{Issue.find(5).subject}/,
386 }
386 }
387 }
387 }
388 }
388 }
389 end
389 end
390
390
391 def test_user_activity
391 def test_user_activity
392 get :activity, :user_id => 2
392 get :activity, :user_id => 2
393 assert_response :success
393 assert_response :success
394 assert_template 'activity'
394 assert_template 'activity'
395 assert_not_nil assigns(:events_by_day)
395 assert_not_nil assigns(:events_by_day)
396
396
397 assert_tag :tag => "h3",
397 assert_tag :tag => "h3",
398 :content => /#{3.day.ago.to_date.day}/,
398 :content => /#{3.day.ago.to_date.day}/,
399 :sibling => { :tag => "dl",
399 :sibling => { :tag => "dl",
400 :child => { :tag => "dt",
400 :child => { :tag => "dt",
401 :attributes => { :class => /issue/ },
401 :attributes => { :class => /issue/ },
402 :child => { :tag => "a",
402 :child => { :tag => "a",
403 :content => /#{Issue.find(1).subject}/,
403 :content => /#{Issue.find(1).subject}/,
404 }
404 }
405 }
405 }
406 }
406 }
407 end
407 end
408
408
409 def test_global_activity_atom_routing
409 def test_global_activity_atom_routing
410 assert_routing({:method => :get, :path => '/activity.atom'}, :controller => 'projects', :action => 'activity', :id => nil, :format => 'atom')
410 assert_routing({:method => :get, :path => '/activity.atom'}, :controller => 'projects', :action => 'activity', :id => nil, :format => 'atom')
411 end
411 end
412
412
413 def test_activity_atom_feed
413 def test_activity_atom_feed
414 get :activity, :format => 'atom'
414 get :activity, :format => 'atom'
415 assert_response :success
415 assert_response :success
416 assert_template 'common/feed.atom.rxml'
416 assert_template 'common/feed.atom.rxml'
417 end
417 end
418
418
419 def test_archive_routing
419 def test_archive_routing
420 assert_routing(
420 assert_routing(
421 #TODO: use PUT to project path and modify form
421 #TODO: use PUT to project path and modify form
422 {:method => :post, :path => 'projects/64/archive'},
422 {:method => :post, :path => 'projects/64/archive'},
423 :controller => 'projects', :action => 'archive', :id => '64'
423 :controller => 'projects', :action => 'archive', :id => '64'
424 )
424 )
425 end
425 end
426
426
427 def test_archive
427 def test_archive
428 @request.session[:user_id] = 1 # admin
428 @request.session[:user_id] = 1 # admin
429 post :archive, :id => 1
429 post :archive, :id => 1
430 assert_redirected_to 'admin/projects'
430 assert_redirected_to 'admin/projects'
431 assert !Project.find(1).active?
431 assert !Project.find(1).active?
432 end
432 end
433
433
434 def test_unarchive_routing
434 def test_unarchive_routing
435 assert_routing(
435 assert_routing(
436 #TODO: use PUT to project path and modify form
436 #TODO: use PUT to project path and modify form
437 {:method => :post, :path => '/projects/567/unarchive'},
437 {:method => :post, :path => '/projects/567/unarchive'},
438 :controller => 'projects', :action => 'unarchive', :id => '567'
438 :controller => 'projects', :action => 'unarchive', :id => '567'
439 )
439 )
440 end
440 end
441
441
442 def test_unarchive
442 def test_unarchive
443 @request.session[:user_id] = 1 # admin
443 @request.session[:user_id] = 1 # admin
444 Project.find(1).archive
444 Project.find(1).archive
445 post :unarchive, :id => 1
445 post :unarchive, :id => 1
446 assert_redirected_to 'admin/projects'
446 assert_redirected_to 'admin/projects'
447 assert Project.find(1).active?
447 assert Project.find(1).active?
448 end
448 end
449
449
450 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
450 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
451 CustomField.delete_all
451 CustomField.delete_all
452 parent = nil
452 parent = nil
453 6.times do |i|
453 6.times do |i|
454 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
454 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
455 p.set_parent!(parent)
455 p.set_parent!(parent)
456
457 get :show, :id => p
456 get :show, :id => p
458 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
457 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
459 :children => { :count => [i, 3].min,
458 :children => { :count => [i, 3].min,
460 :only => { :tag => 'a' } }
459 :only => { :tag => 'a' } }
461
460
462 parent = p
461 parent = p
463 end
462 end
464 end
463 end
465
464
465 def test_copy_with_project
466 @request.session[:user_id] = 1 # admin
467 get :copy, :id => 1
468 assert_response :success
469 assert_template 'copy'
470 assert assigns(:project)
471 assert_equal Project.find(1).description, assigns(:project).description
472 assert_nil assigns(:project).id
473 end
474
475 def test_copy_without_project
476 @request.session[:user_id] = 1 # admin
477 get :copy
478 assert_response :redirect
479 assert_redirected_to :controller => 'admin', :action => 'projects'
480 end
481
466 def test_jump_should_redirect_to_active_tab
482 def test_jump_should_redirect_to_active_tab
467 get :show, :id => 1, :jump => 'issues'
483 get :show, :id => 1, :jump => 'issues'
468 assert_redirected_to 'projects/ecookbook/issues'
484 assert_redirected_to 'projects/ecookbook/issues'
469 end
485 end
470
486
471 def test_jump_should_not_redirect_to_inactive_tab
487 def test_jump_should_not_redirect_to_inactive_tab
472 get :show, :id => 3, :jump => 'documents'
488 get :show, :id => 3, :jump => 'documents'
473 assert_response :success
489 assert_response :success
474 assert_template 'show'
490 assert_template 'show'
475 end
491 end
476
492
477 def test_jump_should_not_redirect_to_unknown_tab
493 def test_jump_should_not_redirect_to_unknown_tab
478 get :show, :id => 3, :jump => 'foobar'
494 get :show, :id => 3, :jump => 'foobar'
479 assert_response :success
495 assert_response :success
480 assert_template 'show'
496 assert_template 'show'
481 end
497 end
482
498
483 # A hook that is manually registered later
499 # A hook that is manually registered later
484 class ProjectBasedTemplate < Redmine::Hook::ViewListener
500 class ProjectBasedTemplate < Redmine::Hook::ViewListener
485 def view_layouts_base_html_head(context)
501 def view_layouts_base_html_head(context)
486 # Adds a project stylesheet
502 # Adds a project stylesheet
487 stylesheet_link_tag(context[:project].identifier) if context[:project]
503 stylesheet_link_tag(context[:project].identifier) if context[:project]
488 end
504 end
489 end
505 end
490 # Don't use this hook now
506 # Don't use this hook now
491 Redmine::Hook.clear_listeners
507 Redmine::Hook.clear_listeners
492
508
493 def test_hook_response
509 def test_hook_response
494 Redmine::Hook.add_listener(ProjectBasedTemplate)
510 Redmine::Hook.add_listener(ProjectBasedTemplate)
495 get :show, :id => 1
511 get :show, :id => 1
496 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
512 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
497 :parent => {:tag => 'head'}
513 :parent => {:tag => 'head'}
498
514
499 Redmine::Hook.clear_listeners
515 Redmine::Hook.clear_listeners
500 end
516 end
501 end
517 end
@@ -1,236 +1,320
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class ProjectTest < Test::Unit::TestCase
20 class ProjectTest < Test::Unit::TestCase
21 fixtures :projects, :enabled_modules,
21 fixtures :projects, :enabled_modules,
22 :issues, :issue_statuses, :journals, :journal_details,
22 :issues, :issue_statuses, :journals, :journal_details,
23 :users, :members, :roles, :projects_trackers, :trackers, :boards
23 :users, :members, :roles, :projects_trackers, :trackers, :boards,
24 :queries
24
25
25 def setup
26 def setup
26 @ecookbook = Project.find(1)
27 @ecookbook = Project.find(1)
27 @ecookbook_sub1 = Project.find(3)
28 @ecookbook_sub1 = Project.find(3)
28 end
29 end
29
30
30 def test_truth
31 def test_truth
31 assert_kind_of Project, @ecookbook
32 assert_kind_of Project, @ecookbook
32 assert_equal "eCookbook", @ecookbook.name
33 assert_equal "eCookbook", @ecookbook.name
33 end
34 end
34
35
35 def test_update
36 def test_update
36 assert_equal "eCookbook", @ecookbook.name
37 assert_equal "eCookbook", @ecookbook.name
37 @ecookbook.name = "eCook"
38 @ecookbook.name = "eCook"
38 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
39 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
39 @ecookbook.reload
40 @ecookbook.reload
40 assert_equal "eCook", @ecookbook.name
41 assert_equal "eCook", @ecookbook.name
41 end
42 end
42
43
43 def test_validate
44 def test_validate
44 @ecookbook.name = ""
45 @ecookbook.name = ""
45 assert !@ecookbook.save
46 assert !@ecookbook.save
46 assert_equal 1, @ecookbook.errors.count
47 assert_equal 1, @ecookbook.errors.count
47 assert_equal I18n.translate('activerecord.errors.messages.blank'), @ecookbook.errors.on(:name)
48 assert_equal I18n.translate('activerecord.errors.messages.blank'), @ecookbook.errors.on(:name)
48 end
49 end
49
50
50 def test_archive
51 def test_archive
51 user = @ecookbook.members.first.user
52 user = @ecookbook.members.first.user
52 @ecookbook.archive
53 @ecookbook.archive
53 @ecookbook.reload
54 @ecookbook.reload
54
55
55 assert !@ecookbook.active?
56 assert !@ecookbook.active?
56 assert !user.projects.include?(@ecookbook)
57 assert !user.projects.include?(@ecookbook)
57 # Subproject are also archived
58 # Subproject are also archived
58 assert !@ecookbook.children.empty?
59 assert !@ecookbook.children.empty?
59 assert @ecookbook.descendants.active.empty?
60 assert @ecookbook.descendants.active.empty?
60 end
61 end
61
62
62 def test_unarchive
63 def test_unarchive
63 user = @ecookbook.members.first.user
64 user = @ecookbook.members.first.user
64 @ecookbook.archive
65 @ecookbook.archive
65 # A subproject of an archived project can not be unarchived
66 # A subproject of an archived project can not be unarchived
66 assert !@ecookbook_sub1.unarchive
67 assert !@ecookbook_sub1.unarchive
67
68
68 # Unarchive project
69 # Unarchive project
69 assert @ecookbook.unarchive
70 assert @ecookbook.unarchive
70 @ecookbook.reload
71 @ecookbook.reload
71 assert @ecookbook.active?
72 assert @ecookbook.active?
72 assert user.projects.include?(@ecookbook)
73 assert user.projects.include?(@ecookbook)
73 # Subproject can now be unarchived
74 # Subproject can now be unarchived
74 @ecookbook_sub1.reload
75 @ecookbook_sub1.reload
75 assert @ecookbook_sub1.unarchive
76 assert @ecookbook_sub1.unarchive
76 end
77 end
77
78
78 def test_destroy
79 def test_destroy
79 # 2 active members
80 # 2 active members
80 assert_equal 2, @ecookbook.members.size
81 assert_equal 2, @ecookbook.members.size
81 # and 1 is locked
82 # and 1 is locked
82 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
83 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
83 # some boards
84 # some boards
84 assert @ecookbook.boards.any?
85 assert @ecookbook.boards.any?
85
86
86 @ecookbook.destroy
87 @ecookbook.destroy
87 # make sure that the project non longer exists
88 # make sure that the project non longer exists
88 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
89 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
89 # make sure related data was removed
90 # make sure related data was removed
90 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
91 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
91 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
92 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
92 end
93 end
93
94
94 def test_move_an_orphan_project_to_a_root_project
95 def test_move_an_orphan_project_to_a_root_project
95 sub = Project.find(2)
96 sub = Project.find(2)
96 sub.set_parent! @ecookbook
97 sub.set_parent! @ecookbook
97 assert_equal @ecookbook.id, sub.parent.id
98 assert_equal @ecookbook.id, sub.parent.id
98 @ecookbook.reload
99 @ecookbook.reload
99 assert_equal 4, @ecookbook.children.size
100 assert_equal 4, @ecookbook.children.size
100 end
101 end
101
102
102 def test_move_an_orphan_project_to_a_subproject
103 def test_move_an_orphan_project_to_a_subproject
103 sub = Project.find(2)
104 sub = Project.find(2)
104 assert sub.set_parent!(@ecookbook_sub1)
105 assert sub.set_parent!(@ecookbook_sub1)
105 end
106 end
106
107
107 def test_move_a_root_project_to_a_project
108 def test_move_a_root_project_to_a_project
108 sub = @ecookbook
109 sub = @ecookbook
109 assert sub.set_parent!(Project.find(2))
110 assert sub.set_parent!(Project.find(2))
110 end
111 end
111
112
112 def test_should_not_move_a_project_to_its_children
113 def test_should_not_move_a_project_to_its_children
113 sub = @ecookbook
114 sub = @ecookbook
114 assert !(sub.set_parent!(Project.find(3)))
115 assert !(sub.set_parent!(Project.find(3)))
115 end
116 end
116
117
117 def test_set_parent_should_add_roots_in_alphabetical_order
118 def test_set_parent_should_add_roots_in_alphabetical_order
118 ProjectCustomField.delete_all
119 ProjectCustomField.delete_all
119 Project.delete_all
120 Project.delete_all
120 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
121 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
121 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
122 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
122 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
123 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
123 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
124 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
124
125
125 assert_equal 4, Project.count
126 assert_equal 4, Project.count
126 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
127 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
127 end
128 end
128
129
129 def test_set_parent_should_add_children_in_alphabetical_order
130 def test_set_parent_should_add_children_in_alphabetical_order
130 ProjectCustomField.delete_all
131 ProjectCustomField.delete_all
131 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
132 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
132 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
133 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
133 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
134 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
134 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
135 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
135 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
136 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
136
137
137 parent.reload
138 parent.reload
138 assert_equal 4, parent.children.size
139 assert_equal 4, parent.children.size
139 assert_equal parent.children.sort_by(&:name), parent.children
140 assert_equal parent.children.sort_by(&:name), parent.children
140 end
141 end
141
142
142 def test_rebuild_should_sort_children_alphabetically
143 def test_rebuild_should_sort_children_alphabetically
143 ProjectCustomField.delete_all
144 ProjectCustomField.delete_all
144 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
145 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
145 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
146 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
146 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
147 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
147 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
148 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
148 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
149 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
149
150
150 Project.update_all("lft = NULL, rgt = NULL")
151 Project.update_all("lft = NULL, rgt = NULL")
151 Project.rebuild!
152 Project.rebuild!
152
153
153 parent.reload
154 parent.reload
154 assert_equal 4, parent.children.size
155 assert_equal 4, parent.children.size
155 assert_equal parent.children.sort_by(&:name), parent.children
156 assert_equal parent.children.sort_by(&:name), parent.children
156 end
157 end
157
158
158 def test_parent
159 def test_parent
159 p = Project.find(6).parent
160 p = Project.find(6).parent
160 assert p.is_a?(Project)
161 assert p.is_a?(Project)
161 assert_equal 5, p.id
162 assert_equal 5, p.id
162 end
163 end
163
164
164 def test_ancestors
165 def test_ancestors
165 a = Project.find(6).ancestors
166 a = Project.find(6).ancestors
166 assert a.first.is_a?(Project)
167 assert a.first.is_a?(Project)
167 assert_equal [1, 5], a.collect(&:id)
168 assert_equal [1, 5], a.collect(&:id)
168 end
169 end
169
170
170 def test_root
171 def test_root
171 r = Project.find(6).root
172 r = Project.find(6).root
172 assert r.is_a?(Project)
173 assert r.is_a?(Project)
173 assert_equal 1, r.id
174 assert_equal 1, r.id
174 end
175 end
175
176
176 def test_children
177 def test_children
177 c = Project.find(1).children
178 c = Project.find(1).children
178 assert c.first.is_a?(Project)
179 assert c.first.is_a?(Project)
179 assert_equal [5, 3, 4], c.collect(&:id)
180 assert_equal [5, 3, 4], c.collect(&:id)
180 end
181 end
181
182
182 def test_descendants
183 def test_descendants
183 d = Project.find(1).descendants
184 d = Project.find(1).descendants
184 assert d.first.is_a?(Project)
185 assert d.first.is_a?(Project)
185 assert_equal [5, 6, 3, 4], d.collect(&:id)
186 assert_equal [5, 6, 3, 4], d.collect(&:id)
186 end
187 end
187
188
188 def test_rolled_up_trackers
189 def test_rolled_up_trackers
189 parent = Project.find(1)
190 parent = Project.find(1)
190 parent.trackers = Tracker.find([1,2])
191 parent.trackers = Tracker.find([1,2])
191 child = parent.children.find(3)
192 child = parent.children.find(3)
192
193
193 assert_equal [1, 2], parent.tracker_ids
194 assert_equal [1, 2], parent.tracker_ids
194 assert_equal [2, 3], child.trackers.collect(&:id)
195 assert_equal [2, 3], child.trackers.collect(&:id)
195
196
196 assert_kind_of Tracker, parent.rolled_up_trackers.first
197 assert_kind_of Tracker, parent.rolled_up_trackers.first
197 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
198 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
198
199
199 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
200 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
200 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
201 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
201 end
202 end
202
203
203 def test_rolled_up_trackers_should_ignore_archived_subprojects
204 def test_rolled_up_trackers_should_ignore_archived_subprojects
204 parent = Project.find(1)
205 parent = Project.find(1)
205 parent.trackers = Tracker.find([1,2])
206 parent.trackers = Tracker.find([1,2])
206 child = parent.children.find(3)
207 child = parent.children.find(3)
207 child.trackers = Tracker.find([1,3])
208 child.trackers = Tracker.find([1,3])
208 parent.children.each(&:archive)
209 parent.children.each(&:archive)
209
210
210 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
211 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
211 end
212 end
212
213
213 def test_next_identifier
214 def test_next_identifier
214 ProjectCustomField.delete_all
215 ProjectCustomField.delete_all
215 Project.create!(:name => 'last', :identifier => 'p2008040')
216 Project.create!(:name => 'last', :identifier => 'p2008040')
216 assert_equal 'p2008041', Project.next_identifier
217 assert_equal 'p2008041', Project.next_identifier
217 end
218 end
218
219
219 def test_next_identifier_first_project
220 def test_next_identifier_first_project
220 Project.delete_all
221 Project.delete_all
221 assert_nil Project.next_identifier
222 assert_nil Project.next_identifier
222 end
223 end
223
224
225
224 def test_enabled_module_names_should_not_recreate_enabled_modules
226 def test_enabled_module_names_should_not_recreate_enabled_modules
225 project = Project.find(1)
227 project = Project.find(1)
226 # Remove one module
228 # Remove one module
227 modules = project.enabled_modules.slice(0..-2)
229 modules = project.enabled_modules.slice(0..-2)
228 assert modules.any?
230 assert modules.any?
229 assert_difference 'EnabledModule.count', -1 do
231 assert_difference 'EnabledModule.count', -1 do
230 project.enabled_module_names = modules.collect(&:name)
232 project.enabled_module_names = modules.collect(&:name)
231 end
233 end
232 project.reload
234 project.reload
233 # Ids should be preserved
235 # Ids should be preserved
234 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
236 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
235 end
237 end
238
239 def test_copy_from_existing_project
240 source_project = Project.find(1)
241 copied_project = Project.copy_from(1)
242
243 assert copied_project
244 # Cleared attributes
245 assert copied_project.id.blank?
246 assert copied_project.name.blank?
247 assert copied_project.identifier.blank?
248
249 # Duplicated attributes
250 assert_equal source_project.description, copied_project.description
251 assert_equal source_project.enabled_modules, copied_project.enabled_modules
252 assert_equal source_project.trackers, copied_project.trackers
253
254 # Default attributes
255 assert_equal 1, copied_project.status
256 end
257
258 # Context: Project#copy
259 def test_copy_should_copy_issues
260 # Setup
261 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
262 source_project = Project.find(2)
263 Project.destroy_all :identifier => "copy-test"
264 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
265 project.trackers = source_project.trackers
266 assert project.valid?
267
268 assert project.issues.empty?
269 assert project.copy(source_project)
270
271 # Tests
272 assert_equal source_project.issues.size, project.issues.size
273 project.issues.each do |issue|
274 assert issue.valid?
275 assert ! issue.assigned_to.blank?
276 assert_equal project, issue.project
277 end
278 end
279
280 def test_copy_should_copy_members
281 # Setup
282 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
283 source_project = Project.find(2)
284 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
285 project.trackers = source_project.trackers
286 project.enabled_modules = source_project.enabled_modules
287 assert project.valid?
288
289 assert project.members.empty?
290 assert project.copy(source_project)
291
292 # Tests
293 assert_equal source_project.members.size, project.members.size
294 project.members.each do |member|
295 assert member
296 assert_equal project, member.project
297 end
298 end
299
300 def test_copy_should_copy_project_level_queries
301 # Setup
302 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
303 source_project = Project.find(2)
304 project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
305 project.trackers = source_project.trackers
306 project.enabled_modules = source_project.enabled_modules
307 assert project.valid?
308
309 assert project.queries.empty?
310 assert project.copy(source_project)
311
312 # Tests
313 assert_equal source_project.queries.size, project.queries.size
314 project.queries.each do |query|
315 assert query
316 assert_equal project, query.project
317 end
318 end
319
236 end
320 end
General Comments 0
You need to be logged in to leave comments. Login now