##// END OF EJS Templates
When a specific TimeEntryActivity are change, associated TimeEntries will be...
Eric Davis -
r2836:37d401ac58c3
parent child
Show More
@@ -1,344 +1,345
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 234
235 235 def save_activities
236 236 if request.post? && params[:enumerations]
237 Project.transaction do
237 238 params[:enumerations].each do |id, activity|
238 @project.update_or_build_time_entry_activity(id, activity)
239 @project.update_or_create_time_entry_activity(id, activity)
240 end
239 241 end
240 @project.save
241 242 end
242 243
243 244 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
244 245 end
245 246
246 247 def reset_activities
247 248 @project.time_entry_activities.each do |time_entry_activity|
248 time_entry_activity.destroy
249 time_entry_activity.destroy(time_entry_activity.parent)
249 250 end
250 251 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
251 252 end
252 253
253 254 def list_files
254 255 sort_init 'filename', 'asc'
255 256 sort_update 'filename' => "#{Attachment.table_name}.filename",
256 257 'created_on' => "#{Attachment.table_name}.created_on",
257 258 'size' => "#{Attachment.table_name}.filesize",
258 259 'downloads' => "#{Attachment.table_name}.downloads"
259 260
260 261 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
261 262 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
262 263 render :layout => !request.xhr?
263 264 end
264 265
265 266 # Show changelog for @project
266 267 def changelog
267 268 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
268 269 retrieve_selected_tracker_ids(@trackers)
269 270 @versions = @project.versions.sort
270 271 end
271 272
272 273 def roadmap
273 274 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
274 275 retrieve_selected_tracker_ids(@trackers)
275 276 @versions = @project.versions.sort
276 277 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
277 278 end
278 279
279 280 def activity
280 281 @days = Setting.activity_days_default.to_i
281 282
282 283 if params[:from]
283 284 begin; @date_to = params[:from].to_date + 1; rescue; end
284 285 end
285 286
286 287 @date_to ||= Date.today + 1
287 288 @date_from = @date_to - @days
288 289 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
289 290 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
290 291
291 292 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
292 293 :with_subprojects => @with_subprojects,
293 294 :author => @author)
294 295 @activity.scope_select {|t| !params["show_#{t}"].nil?}
295 296 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
296 297
297 298 events = @activity.events(@date_from, @date_to)
298 299
299 300 respond_to do |format|
300 301 format.html {
301 302 @events_by_day = events.group_by(&:event_date)
302 303 render :layout => false if request.xhr?
303 304 }
304 305 format.atom {
305 306 title = l(:label_activity)
306 307 if @author
307 308 title = @author.name
308 309 elsif @activity.scope.size == 1
309 310 title = l("label_#{@activity.scope.first.singularize}_plural")
310 311 end
311 312 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
312 313 }
313 314 end
314 315
315 316 rescue ActiveRecord::RecordNotFound
316 317 render_404
317 318 end
318 319
319 320 private
320 321 # Find project of id params[:id]
321 322 # if not found, redirect to project list
322 323 # Used as a before_filter
323 324 def find_project
324 325 @project = Project.find(params[:id])
325 326 rescue ActiveRecord::RecordNotFound
326 327 render_404
327 328 end
328 329
329 330 def find_optional_project
330 331 return true unless params[:id]
331 332 @project = Project.find(params[:id])
332 333 authorize
333 334 rescue ActiveRecord::RecordNotFound
334 335 render_404
335 336 end
336 337
337 338 def retrieve_selected_tracker_ids(selectable_trackers)
338 339 if ids = params[:tracker_ids]
339 340 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
340 341 else
341 342 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
342 343 end
343 344 end
344 345 end
@@ -1,527 +1,538
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 # Project statuses
20 20 STATUS_ACTIVE = 1
21 21 STATUS_ARCHIVED = 9
22 22
23 23 # Specific overidden Activities
24 24 has_many :time_entry_activities do
25 25 def active
26 26 find(:all, :conditions => {:active => true})
27 27 end
28 28 end
29 29 has_many :members, :include => :user, :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
30 30 has_many :member_principals, :class_name => 'Member',
31 31 :include => :principal,
32 32 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
33 33 has_many :users, :through => :members
34 34 has_many :principals, :through => :member_principals, :source => :principal
35 35
36 36 has_many :enabled_modules, :dependent => :delete_all
37 37 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
38 38 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
39 39 has_many :issue_changes, :through => :issues, :source => :journals
40 40 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
41 41 has_many :time_entries, :dependent => :delete_all
42 42 has_many :queries, :dependent => :delete_all
43 43 has_many :documents, :dependent => :destroy
44 44 has_many :news, :dependent => :delete_all, :include => :author
45 45 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
46 46 has_many :boards, :dependent => :destroy, :order => "position ASC"
47 47 has_one :repository, :dependent => :destroy
48 48 has_many :changesets, :through => :repository
49 49 has_one :wiki, :dependent => :destroy
50 50 # Custom field for the project issues
51 51 has_and_belongs_to_many :issue_custom_fields,
52 52 :class_name => 'IssueCustomField',
53 53 :order => "#{CustomField.table_name}.position",
54 54 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
55 55 :association_foreign_key => 'custom_field_id'
56 56
57 57 acts_as_nested_set :order => 'name', :dependent => :destroy
58 58 acts_as_attachable :view_permission => :view_files,
59 59 :delete_permission => :manage_files
60 60
61 61 acts_as_customizable
62 62 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
63 63 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
64 64 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
65 65 :author => nil
66 66
67 67 attr_protected :status, :enabled_module_names
68 68
69 69 validates_presence_of :name, :identifier
70 70 validates_uniqueness_of :name, :identifier
71 71 validates_associated :repository, :wiki
72 72 validates_length_of :name, :maximum => 30
73 73 validates_length_of :homepage, :maximum => 255
74 74 validates_length_of :identifier, :in => 1..20
75 75 # donwcase letters, digits, dashes but not digits only
76 76 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
77 77 # reserved words
78 78 validates_exclusion_of :identifier, :in => %w( new )
79 79
80 80 before_destroy :delete_all_members
81 81
82 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] } }
83 83 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
84 84 named_scope :all_public, { :conditions => { :is_public => true } }
85 85 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
86 86
87 87 def identifier=(identifier)
88 88 super unless identifier_frozen?
89 89 end
90 90
91 91 def identifier_frozen?
92 92 errors[:identifier].nil? && !(new_record? || identifier.blank?)
93 93 end
94 94
95 95 def issues_with_subprojects(include_subprojects=false)
96 96 conditions = nil
97 97 if include_subprojects
98 98 ids = [id] + descendants.collect(&:id)
99 99 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
100 100 end
101 101 conditions ||= ["#{Project.table_name}.id = ?", id]
102 102 # Quick and dirty fix for Rails 2 compatibility
103 103 Issue.send(:with_scope, :find => { :conditions => conditions }) do
104 104 Version.send(:with_scope, :find => { :conditions => conditions }) do
105 105 yield
106 106 end
107 107 end
108 108 end
109 109
110 110 # returns latest created projects
111 111 # non public projects will be returned only if user is a member of those
112 112 def self.latest(user=nil, count=5)
113 113 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
114 114 end
115 115
116 116 # Returns a SQL :conditions string used to find all active projects for the specified user.
117 117 #
118 118 # Examples:
119 119 # Projects.visible_by(admin) => "projects.status = 1"
120 120 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
121 121 def self.visible_by(user=nil)
122 122 user ||= User.current
123 123 if user && user.admin?
124 124 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
125 125 elsif user && user.memberships.any?
126 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(',')}))"
127 127 else
128 128 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
129 129 end
130 130 end
131 131
132 132 def self.allowed_to_condition(user, permission, options={})
133 133 statements = []
134 134 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
135 135 if perm = Redmine::AccessControl.permission(permission)
136 136 unless perm.project_module.nil?
137 137 # If the permission belongs to a project module, make sure the module is enabled
138 138 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
139 139 end
140 140 end
141 141 if options[:project]
142 142 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
143 143 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
144 144 base_statement = "(#{project_statement}) AND (#{base_statement})"
145 145 end
146 146 if user.admin?
147 147 # no restriction
148 148 else
149 149 statements << "1=0"
150 150 if user.logged?
151 151 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
152 152 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
153 153 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
154 154 elsif Role.anonymous.allowed_to?(permission)
155 155 # anonymous user allowed on public project
156 156 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
157 157 else
158 158 # anonymous user is not authorized
159 159 end
160 160 end
161 161 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
162 162 end
163 163
164 164 # Returns the Systemwide and project specific activities
165 165 def activities(include_inactive=false)
166 166 if include_inactive
167 167 return all_activities
168 168 else
169 169 return active_activities
170 170 end
171 171 end
172 172
173 # Will build a new Project specific Activity or update an existing one
174 def update_or_build_time_entry_activity(id, activity_hash)
173 # Will create a new Project specific Activity or update an existing one
174 #
175 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
176 # does not successfully save.
177 def update_or_create_time_entry_activity(id, activity_hash)
175 178 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
176 self.build_time_entry_activity_if_needed(activity_hash)
179 self.create_time_entry_activity_if_needed(activity_hash)
177 180 else
178 181 activity = project.time_entry_activities.find_by_id(id.to_i)
179 182 activity.update_attributes(activity_hash) if activity
180 183 end
181 184 end
182 185
183 # Builds new activity
184 def build_time_entry_activity_if_needed(activity)
185 # Only new override activities are built
186 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
187 #
188 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
189 # does not successfully save.
190 def create_time_entry_activity_if_needed(activity)
186 191 if activity['parent_id']
187 192
188 193 parent_activity = TimeEntryActivity.find(activity['parent_id'])
189 194 activity['name'] = parent_activity.name
190 195 activity['position'] = parent_activity.position
191 196
192 197 if Enumeration.overridding_change?(activity, parent_activity)
193 self.time_entry_activities.build(activity)
198 project_activity = self.time_entry_activities.create(activity)
199
200 if project_activity.new_record?
201 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
202 else
203 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
204 end
194 205 end
195 206 end
196 207 end
197 208
198 209 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
199 210 #
200 211 # Examples:
201 212 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
202 213 # project.project_condition(false) => "projects.id = 1"
203 214 def project_condition(with_subprojects)
204 215 cond = "#{Project.table_name}.id = #{id}"
205 216 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
206 217 cond
207 218 end
208 219
209 220 def self.find(*args)
210 221 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
211 222 project = find_by_identifier(*args)
212 223 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
213 224 project
214 225 else
215 226 super
216 227 end
217 228 end
218 229
219 230 def to_param
220 231 # id is used for projects with a numeric identifier (compatibility)
221 232 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
222 233 end
223 234
224 235 def active?
225 236 self.status == STATUS_ACTIVE
226 237 end
227 238
228 239 # Archives the project and its descendants recursively
229 240 def archive
230 241 # Archive subprojects if any
231 242 children.each do |subproject|
232 243 subproject.archive
233 244 end
234 245 update_attribute :status, STATUS_ARCHIVED
235 246 end
236 247
237 248 # Unarchives the project
238 249 # All its ancestors must be active
239 250 def unarchive
240 251 return false if ancestors.detect {|a| !a.active?}
241 252 update_attribute :status, STATUS_ACTIVE
242 253 end
243 254
244 255 # Returns an array of projects the project can be moved to
245 256 def possible_parents
246 257 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
247 258 end
248 259
249 260 # Sets the parent of the project
250 261 # Argument can be either a Project, a String, a Fixnum or nil
251 262 def set_parent!(p)
252 263 unless p.nil? || p.is_a?(Project)
253 264 if p.to_s.blank?
254 265 p = nil
255 266 else
256 267 p = Project.find_by_id(p)
257 268 return false unless p
258 269 end
259 270 end
260 271 if p == parent && !p.nil?
261 272 # Nothing to do
262 273 true
263 274 elsif p.nil? || (p.active? && move_possible?(p))
264 275 # Insert the project so that target's children or root projects stay alphabetically sorted
265 276 sibs = (p.nil? ? self.class.roots : p.children)
266 277 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
267 278 if to_be_inserted_before
268 279 move_to_left_of(to_be_inserted_before)
269 280 elsif p.nil?
270 281 if sibs.empty?
271 282 # move_to_root adds the project in first (ie. left) position
272 283 move_to_root
273 284 else
274 285 move_to_right_of(sibs.last) unless self == sibs.last
275 286 end
276 287 else
277 288 # move_to_child_of adds the project in last (ie.right) position
278 289 move_to_child_of(p)
279 290 end
280 291 true
281 292 else
282 293 # Can not move to the given target
283 294 false
284 295 end
285 296 end
286 297
287 298 # Returns an array of the trackers used by the project and its active sub projects
288 299 def rolled_up_trackers
289 300 @rolled_up_trackers ||=
290 301 Tracker.find(:all, :include => :projects,
291 302 :select => "DISTINCT #{Tracker.table_name}.*",
292 303 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
293 304 :order => "#{Tracker.table_name}.position")
294 305 end
295 306
296 307 # Returns a hash of project users grouped by role
297 308 def users_by_role
298 309 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
299 310 m.roles.each do |r|
300 311 h[r] ||= []
301 312 h[r] << m.user
302 313 end
303 314 h
304 315 end
305 316 end
306 317
307 318 # Deletes all project's members
308 319 def delete_all_members
309 320 me, mr = Member.table_name, MemberRole.table_name
310 321 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
311 322 Member.delete_all(['project_id = ?', id])
312 323 end
313 324
314 325 # Users issues can be assigned to
315 326 def assignable_users
316 327 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
317 328 end
318 329
319 330 # Returns the mail adresses of users that should be always notified on project events
320 331 def recipients
321 332 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
322 333 end
323 334
324 335 # Returns an array of all custom fields enabled for project issues
325 336 # (explictly associated custom fields and custom fields enabled for all projects)
326 337 def all_issue_custom_fields
327 338 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
328 339 end
329 340
330 341 def project
331 342 self
332 343 end
333 344
334 345 def <=>(project)
335 346 name.downcase <=> project.name.downcase
336 347 end
337 348
338 349 def to_s
339 350 name
340 351 end
341 352
342 353 # Returns a short description of the projects (first lines)
343 354 def short_description(length = 255)
344 355 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
345 356 end
346 357
347 358 # Return true if this project is allowed to do the specified action.
348 359 # action can be:
349 360 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
350 361 # * a permission Symbol (eg. :edit_project)
351 362 def allows_to?(action)
352 363 if action.is_a? Hash
353 364 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
354 365 else
355 366 allowed_permissions.include? action
356 367 end
357 368 end
358 369
359 370 def module_enabled?(module_name)
360 371 module_name = module_name.to_s
361 372 enabled_modules.detect {|m| m.name == module_name}
362 373 end
363 374
364 375 def enabled_module_names=(module_names)
365 376 if module_names && module_names.is_a?(Array)
366 377 module_names = module_names.collect(&:to_s)
367 378 # remove disabled modules
368 379 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
369 380 # add new modules
370 381 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
371 382 else
372 383 enabled_modules.clear
373 384 end
374 385 end
375 386
376 387 # Returns an auto-generated project identifier based on the last identifier used
377 388 def self.next_identifier
378 389 p = Project.find(:first, :order => 'created_on DESC')
379 390 p.nil? ? nil : p.identifier.to_s.succ
380 391 end
381 392
382 393 # Copies and saves the Project instance based on the +project+.
383 394 # Will duplicate the source project's:
384 395 # * Issues
385 396 # * Members
386 397 # * Queries
387 398 def copy(project)
388 399 project = project.is_a?(Project) ? project : Project.find(project)
389 400
390 401 Project.transaction do
391 402 # Wikis
392 403 self.wiki = Wiki.new(project.wiki.attributes.dup.except("project_id"))
393 404 project.wiki.pages.each do |page|
394 405 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("page_id"))
395 406 new_wiki_page = WikiPage.new(page.attributes.dup.except("wiki_id"))
396 407 new_wiki_page.content = new_wiki_content
397 408
398 409 self.wiki.pages << new_wiki_page
399 410 end
400 411
401 412 # Versions
402 413 project.versions.each do |version|
403 414 new_version = Version.new
404 415 new_version.attributes = version.attributes.dup.except("project_id")
405 416 self.versions << new_version
406 417 end
407 418
408 419 project.issue_categories.each do |issue_category|
409 420 new_issue_category = IssueCategory.new
410 421 new_issue_category.attributes = issue_category.attributes.dup.except("project_id")
411 422 self.issue_categories << new_issue_category
412 423 end
413 424
414 425 # Issues
415 426 project.issues.each do |issue|
416 427 new_issue = Issue.new
417 428 new_issue.copy_from(issue)
418 429 # Reassign fixed_versions by name, since names are unique per
419 430 # project and the versions for self are not yet saved
420 431 if issue.fixed_version
421 432 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
422 433 end
423 434 # Reassign the category by name, since names are unique per
424 435 # project and the categories for self are not yet saved
425 436 if issue.category
426 437 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
427 438 end
428 439
429 440 self.issues << new_issue
430 441 end
431 442
432 443 # Members
433 444 project.members.each do |member|
434 445 new_member = Member.new
435 446 new_member.attributes = member.attributes.dup.except("project_id")
436 447 new_member.role_ids = member.role_ids.dup
437 448 new_member.project = self
438 449 self.members << new_member
439 450 end
440 451
441 452 # Queries
442 453 project.queries.each do |query|
443 454 new_query = Query.new
444 455 new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria")
445 456 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
446 457 new_query.project = self
447 458 self.queries << new_query
448 459 end
449 460
450 461 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
451 462 self.save
452 463 end
453 464 end
454 465
455 466
456 467 # Copies +project+ and returns the new instance. This will not save
457 468 # the copy
458 469 def self.copy_from(project)
459 470 begin
460 471 project = project.is_a?(Project) ? project : Project.find(project)
461 472 if project
462 473 # clear unique attributes
463 474 attributes = project.attributes.dup.except('name', 'identifier', 'id', 'status')
464 475 copy = Project.new(attributes)
465 476 copy.enabled_modules = project.enabled_modules
466 477 copy.trackers = project.trackers
467 478 copy.custom_values = project.custom_values.collect {|v| v.clone}
468 479 copy.issue_custom_fields = project.issue_custom_fields
469 480 return copy
470 481 else
471 482 return nil
472 483 end
473 484 rescue ActiveRecord::RecordNotFound
474 485 return nil
475 486 end
476 487 end
477 488
478 489 private
479 490 def allowed_permissions
480 491 @allowed_permissions ||= begin
481 492 module_names = enabled_modules.collect {|m| m.name}
482 493 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
483 494 end
484 495 end
485 496
486 497 def allowed_actions
487 498 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
488 499 end
489 500
490 501 # Returns all the active Systemwide and project specific activities
491 502 def active_activities
492 503 overridden_activity_ids = self.time_entry_activities.active.collect(&:parent_id)
493 504
494 505 if overridden_activity_ids.empty?
495 506 return TimeEntryActivity.active
496 507 else
497 508 return system_activities_and_project_overrides
498 509 end
499 510 end
500 511
501 512 # Returns all the Systemwide and project specific activities
502 513 # (inactive and active)
503 514 def all_activities
504 515 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
505 516
506 517 if overridden_activity_ids.empty?
507 518 return TimeEntryActivity.all
508 519 else
509 520 return system_activities_and_project_overrides(true)
510 521 end
511 522 end
512 523
513 524 # Returns the systemwide active activities merged with the project specific overrides
514 525 def system_activities_and_project_overrides(include_inactive=false)
515 526 if include_inactive
516 527 return TimeEntryActivity.all.
517 528 find(:all,
518 529 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
519 530 self.time_entry_activities
520 531 else
521 532 return TimeEntryActivity.active.
522 533 find(:all,
523 534 :conditions => ["id NOT IN (?)", self.time_entry_activities.active.collect(&:parent_id)]) +
524 535 self.time_entry_activities.active
525 536 end
526 537 end
527 538 end
1 NO CONTENT: modified file
@@ -1,704 +1,765
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, :custom_fields, :custom_values
27 :attachments, :custom_fields, :custom_values, :time_entries
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 558
559 559 def test_reset_activities_routing
560 560 assert_routing({:method => :delete, :path => 'projects/64/reset_activities'},
561 561 :controller => 'projects', :action => 'reset_activities', :id => '64')
562 562 end
563 563
564 564 def test_reset_activities
565 565 @request.session[:user_id] = 2 # manager
566 566 project_activity = TimeEntryActivity.new({
567 567 :name => 'Project Specific',
568 568 :parent => TimeEntryActivity.find(:first),
569 569 :project => Project.find(1),
570 570 :active => true
571 571 })
572 572 assert project_activity.save
573 573 project_activity_two = TimeEntryActivity.new({
574 574 :name => 'Project Specific Two',
575 575 :parent => TimeEntryActivity.find(:last),
576 576 :project => Project.find(1),
577 577 :active => true
578 578 })
579 579 assert project_activity_two.save
580 580
581 581 delete :reset_activities, :id => 1
582 582 assert_response :redirect
583 583 assert_redirected_to 'projects/ecookbook/settings/activities'
584 584
585 585 assert_nil TimeEntryActivity.find_by_id(project_activity.id)
586 586 assert_nil TimeEntryActivity.find_by_id(project_activity_two.id)
587 587 end
588 588
589 def test_reset_activities_should_reassign_time_entries_back_to_the_system_activity
590 @request.session[:user_id] = 2 # manager
591 project_activity = TimeEntryActivity.new({
592 :name => 'Project Specific Design',
593 :parent => TimeEntryActivity.find(9),
594 :project => Project.find(1),
595 :active => true
596 })
597 assert project_activity.save
598 assert TimeEntry.update_all("activity_id = '#{project_activity.id}'", ["project_id = ? AND activity_id = ?", 1, 9])
599 assert 3, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size
600
601 delete :reset_activities, :id => 1
602 assert_response :redirect
603 assert_redirected_to 'projects/ecookbook/settings/activities'
604
605 assert_nil TimeEntryActivity.find_by_id(project_activity.id)
606 assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size, "TimeEntries still assigned to project specific activity"
607 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "TimeEntries still assigned to project specific activity"
608 end
609
589 610 def test_save_activities_routing
590 611 assert_routing({:method => :post, :path => 'projects/64/activities/save'},
591 612 :controller => 'projects', :action => 'save_activities', :id => '64')
592 613 end
593 614
594 615 def test_save_activities_to_override_system_activities
595 616 @request.session[:user_id] = 2 # manager
596 617 billable_field = TimeEntryActivityCustomField.find_by_name("Billable")
597 618
598 619 post :save_activities, :id => 1, :enumerations => {
599 620 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design, De-activate
600 621 "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"}, # Development, Change custom value
601 622 "14"=>{"parent_id"=>"14", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"}, # Inactive Activity, Activate with custom value
602 623 "11"=>{"parent_id"=>"11", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"} # QA, no changes
603 624 }
604 625
605 626 assert_response :redirect
606 627 assert_redirected_to 'projects/ecookbook/settings/activities'
607 628
608 629 # Created project specific activities...
609 630 project = Project.find('ecookbook')
610 631
611 632 # ... Design
612 633 design = project.time_entry_activities.find_by_name("Design")
613 634 assert design, "Project activity not found"
614 635
615 636 assert_equal 9, design.parent_id # Relate to the system activity
616 637 assert_not_equal design.parent.id, design.id # Different records
617 638 assert_equal design.parent.name, design.name # Same name
618 639 assert !design.active?
619 640
620 641 # ... Development
621 642 development = project.time_entry_activities.find_by_name("Development")
622 643 assert development, "Project activity not found"
623 644
624 645 assert_equal 10, development.parent_id # Relate to the system activity
625 646 assert_not_equal development.parent.id, development.id # Different records
626 647 assert_equal development.parent.name, development.name # Same name
627 648 assert development.active?
628 649 assert_equal "0", development.custom_value_for(billable_field).value
629 650
630 651 # ... Inactive Activity
631 652 previously_inactive = project.time_entry_activities.find_by_name("Inactive Activity")
632 653 assert previously_inactive, "Project activity not found"
633 654
634 655 assert_equal 14, previously_inactive.parent_id # Relate to the system activity
635 656 assert_not_equal previously_inactive.parent.id, previously_inactive.id # Different records
636 657 assert_equal previously_inactive.parent.name, previously_inactive.name # Same name
637 658 assert previously_inactive.active?
638 659 assert_equal "1", previously_inactive.custom_value_for(billable_field).value
639 660
640 661 # ... QA
641 662 assert_equal nil, project.time_entry_activities.find_by_name("QA"), "Custom QA activity created when it wasn't modified"
642 663 end
643 664
644 665 def test_save_activities_will_update_project_specific_activities
645 666 @request.session[:user_id] = 2 # manager
646 667
647 668 project_activity = TimeEntryActivity.new({
648 669 :name => 'Project Specific',
649 670 :parent => TimeEntryActivity.find(:first),
650 671 :project => Project.find(1),
651 672 :active => true
652 673 })
653 674 assert project_activity.save
654 675 project_activity_two = TimeEntryActivity.new({
655 676 :name => 'Project Specific Two',
656 677 :parent => TimeEntryActivity.find(:last),
657 678 :project => Project.find(1),
658 679 :active => true
659 680 })
660 681 assert project_activity_two.save
661 682
662 683
663 684 post :save_activities, :id => 1, :enumerations => {
664 685 project_activity.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # De-activate
665 686 project_activity_two.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"} # De-activate
666 687 }
667 688
668 689 assert_response :redirect
669 690 assert_redirected_to 'projects/ecookbook/settings/activities'
670 691
671 692 # Created project specific activities...
672 693 project = Project.find('ecookbook')
673 694 assert_equal 2, project.time_entry_activities.count
674 695
675 696 activity_one = project.time_entry_activities.find_by_name(project_activity.name)
676 697 assert activity_one, "Project activity not found"
677 698 assert_equal project_activity.id, activity_one.id
678 699 assert !activity_one.active?
679 700
680 701 activity_two = project.time_entry_activities.find_by_name(project_activity_two.name)
681 702 assert activity_two, "Project activity not found"
682 703 assert_equal project_activity_two.id, activity_two.id
683 704 assert !activity_two.active?
684 705 end
685 706
707 def test_save_activities_when_creating_new_activities_will_convert_existing_data
708 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
709
710 @request.session[:user_id] = 2 # manager
711 post :save_activities, :id => 1, :enumerations => {
712 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"} # Design, De-activate
713 }
714 assert_response :redirect
715
716 # No more TimeEntries using the system activity
717 assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries still assigned to system activities"
718 # All TimeEntries using project activity
719 project_specific_activity = TimeEntryActivity.find_by_parent_id_and_project_id(9, 1)
720 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_specific_activity.id, 1).size, "No Time Entries assigned to the project activity"
721 end
722
723 def test_save_activities_when_creating_new_activities_will_not_convert_existing_data_if_an_exception_is_raised
724 # TODO: Need to cause an exception on create but these tests
725 # aren't setup for mocking. Just create a record now so the
726 # second one is a dupicate
727 parent = TimeEntryActivity.find(9)
728 TimeEntryActivity.create!({:name => parent.name, :project_id => 1, :position => parent.position, :active => true})
729 TimeEntry.create!({:project_id => 1, :hours => 1.0, :user => User.find(1), :issue_id => 3, :activity_id => 10, :spent_on => '2009-01-01'})
730
731 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
732 assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size
733
734 @request.session[:user_id] = 2 # manager
735 post :save_activities, :id => 1, :enumerations => {
736 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design
737 "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"} # Development, Change custom value
738 }
739 assert_response :redirect
740
741 # TimeEntries shouldn't have been reassigned on the failed record
742 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries are not assigned to system activities"
743 # TimeEntries shouldn't have been reassigned on the saved record either
744 assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size, "Time Entries are not assigned to system activities"
745 end
746
686 747 # A hook that is manually registered later
687 748 class ProjectBasedTemplate < Redmine::Hook::ViewListener
688 749 def view_layouts_base_html_head(context)
689 750 # Adds a project stylesheet
690 751 stylesheet_link_tag(context[:project].identifier) if context[:project]
691 752 end
692 753 end
693 754 # Don't use this hook now
694 755 Redmine::Hook.clear_listeners
695 756
696 757 def test_hook_response
697 758 Redmine::Hook.add_listener(ProjectBasedTemplate)
698 759 get :show, :id => 1
699 760 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
700 761 :parent => {:tag => 'head'}
701 762
702 763 Redmine::Hook.clear_listeners
703 764 end
704 765 end
General Comments 0
You need to be logged in to leave comments. Login now