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