##// END OF EJS Templates
Cleans up parent project assignment in ProjectsController....
Jean-Philippe Lang -
r13465:5d2eea14893b
parent child
Show More
@@ -1,253 +1,233
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 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 :settings, :only => :settings
21 21
22 22 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
23 23 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
24 24 before_filter :authorize_global, :only => [:new, :create]
25 25 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
26 26 accept_rss_auth :index
27 27 accept_api_auth :index, :show, :create, :update, :destroy
28 28
29 29 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
30 30 if controller.request.post?
31 31 controller.send :expire_action, :controller => 'welcome', :action => 'robots'
32 32 end
33 33 end
34 34
35 35 helper :custom_fields
36 36 helper :issues
37 37 helper :queries
38 38 helper :repositories
39 39 helper :members
40 40
41 41 # Lists visible projects
42 42 def index
43 43 scope = Project.visible.sorted
44 44
45 45 respond_to do |format|
46 46 format.html {
47 47 unless params[:closed]
48 48 scope = scope.active
49 49 end
50 50 @projects = scope.to_a
51 51 }
52 52 format.api {
53 53 @offset, @limit = api_offset_and_limit
54 54 @project_count = scope.count
55 55 @projects = scope.offset(@offset).limit(@limit).to_a
56 56 }
57 57 format.atom {
58 58 projects = scope.reorder(:created_on => :desc).limit(Setting.feeds_limit.to_i).to_a
59 59 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
60 60 }
61 61 end
62 62 end
63 63
64 64 def new
65 65 @issue_custom_fields = IssueCustomField.sorted.to_a
66 66 @trackers = Tracker.sorted.to_a
67 67 @project = Project.new
68 68 @project.safe_attributes = params[:project]
69 69 end
70 70
71 71 def create
72 72 @issue_custom_fields = IssueCustomField.sorted.to_a
73 73 @trackers = Tracker.sorted.to_a
74 74 @project = Project.new
75 75 @project.safe_attributes = params[:project]
76 76
77 if validate_parent_id && @project.save
78 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
77 if @project.save
79 78 unless User.current.admin?
80 79 @project.add_default_member(User.current)
81 80 end
82 81 respond_to do |format|
83 82 format.html {
84 83 flash[:notice] = l(:notice_successful_create)
85 84 if params[:continue]
86 85 attrs = {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}
87 86 redirect_to new_project_path(attrs)
88 87 else
89 88 redirect_to settings_project_path(@project)
90 89 end
91 90 }
92 91 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
93 92 end
94 93 else
95 94 respond_to do |format|
96 95 format.html { render :action => 'new' }
97 96 format.api { render_validation_errors(@project) }
98 97 end
99 98 end
100 99 end
101 100
102 101 def copy
103 102 @issue_custom_fields = IssueCustomField.sorted.to_a
104 103 @trackers = Tracker.sorted.to_a
105 104 @source_project = Project.find(params[:id])
106 105 if request.get?
107 106 @project = Project.copy_from(@source_project)
108 107 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
109 108 else
110 109 Mailer.with_deliveries(params[:notifications] == '1') do
111 110 @project = Project.new
112 111 @project.safe_attributes = params[:project]
113 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
114 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
112 if @project.copy(@source_project, :only => params[:only])
115 113 flash[:notice] = l(:notice_successful_create)
116 114 redirect_to settings_project_path(@project)
117 115 elsif !@project.new_record?
118 116 # Project was created
119 117 # But some objects were not copied due to validation failures
120 118 # (eg. issues from disabled trackers)
121 119 # TODO: inform about that
122 120 redirect_to settings_project_path(@project)
123 121 end
124 122 end
125 123 end
126 124 rescue ActiveRecord::RecordNotFound
127 125 # source_project not found
128 126 render_404
129 127 end
130 128
131 129 # Show @project
132 130 def show
133 131 # try to redirect to the requested menu item
134 132 if params[:jump] && redirect_to_project_menu_item(@project, params[:jump])
135 133 return
136 134 end
137 135
138 136 @users_by_role = @project.users_by_role
139 137 @subprojects = @project.children.visible.to_a
140 138 @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").to_a
141 139 @trackers = @project.rolled_up_trackers
142 140
143 141 cond = @project.project_condition(Setting.display_subprojects_issues?)
144 142
145 143 @open_issues_by_tracker = Issue.visible.open.where(cond).group(:tracker).count
146 144 @total_issues_by_tracker = Issue.visible.where(cond).group(:tracker).count
147 145
148 146 if User.current.allowed_to?(:view_time_entries, @project)
149 147 @total_hours = TimeEntry.visible.where(cond).sum(:hours).to_f
150 148 end
151 149
152 150 @key = User.current.rss_key
153 151
154 152 respond_to do |format|
155 153 format.html
156 154 format.api
157 155 end
158 156 end
159 157
160 158 def settings
161 159 @issue_custom_fields = IssueCustomField.sorted.to_a
162 160 @issue_category ||= IssueCategory.new
163 161 @member ||= @project.members.new
164 162 @trackers = Tracker.sorted.to_a
165 163 @wiki ||= @project.wiki || Wiki.new(:project => @project)
166 164 end
167 165
168 166 def edit
169 167 end
170 168
171 169 def update
172 170 @project.safe_attributes = params[:project]
173 if validate_parent_id && @project.save
174 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
171 if @project.save
175 172 respond_to do |format|
176 173 format.html {
177 174 flash[:notice] = l(:notice_successful_update)
178 175 redirect_to settings_project_path(@project)
179 176 }
180 177 format.api { render_api_ok }
181 178 end
182 179 else
183 180 respond_to do |format|
184 181 format.html {
185 182 settings
186 183 render :action => 'settings'
187 184 }
188 185 format.api { render_validation_errors(@project) }
189 186 end
190 187 end
191 188 end
192 189
193 190 def modules
194 191 @project.enabled_module_names = params[:enabled_module_names]
195 192 flash[:notice] = l(:notice_successful_update)
196 193 redirect_to settings_project_path(@project, :tab => 'modules')
197 194 end
198 195
199 196 def archive
200 197 unless @project.archive
201 198 flash[:error] = l(:error_can_not_archive_project)
202 199 end
203 200 redirect_to admin_projects_path(:status => params[:status])
204 201 end
205 202
206 203 def unarchive
207 204 unless @project.active?
208 205 @project.unarchive
209 206 end
210 207 redirect_to admin_projects_path(:status => params[:status])
211 208 end
212 209
213 210 def close
214 211 @project.close
215 212 redirect_to project_path(@project)
216 213 end
217 214
218 215 def reopen
219 216 @project.reopen
220 217 redirect_to project_path(@project)
221 218 end
222 219
223 220 # Delete @project
224 221 def destroy
225 222 @project_to_destroy = @project
226 223 if api_request? || params[:confirm]
227 224 @project_to_destroy.destroy
228 225 respond_to do |format|
229 226 format.html { redirect_to admin_projects_path }
230 227 format.api { render_api_ok }
231 228 end
232 229 end
233 230 # hide project in layout
234 231 @project = nil
235 232 end
236
237 private
238
239 # Validates parent_id param according to user's permissions
240 # TODO: move it to Project model in a validation that depends on User.current
241 def validate_parent_id
242 return true if User.current.admin?
243 parent_id = params[:project] && params[:project][:parent_id]
244 if parent_id || @project.new_record?
245 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
246 unless @project.allowed_parents.include?(parent)
247 @project.errors.add :parent_id, :invalid
248 return false
249 end
250 end
251 true
252 end
253 233 end
@@ -1,1024 +1,1030
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 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 include Redmine::SafeAttributes
20 20 include Redmine::NestedSet::ProjectNestedSet
21 21
22 22 # Project statuses
23 23 STATUS_ACTIVE = 1
24 24 STATUS_CLOSED = 5
25 25 STATUS_ARCHIVED = 9
26 26
27 27 # Maximum length for project identifiers
28 28 IDENTIFIER_MAX_LENGTH = 100
29 29
30 30 # Specific overridden Activities
31 31 has_many :time_entry_activities
32 32 has_many :members,
33 33 lambda { joins(:principal, :roles).
34 34 where("#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}") }
35 35 has_many :memberships, :class_name => 'Member'
36 36 has_many :member_principals,
37 37 lambda { joins(:principal).
38 38 where("#{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}")},
39 39 :class_name => 'Member'
40 40 has_many :enabled_modules, :dependent => :delete_all
41 41 has_and_belongs_to_many :trackers, lambda {order(:position)}
42 42 has_many :issues, :dependent => :destroy
43 43 has_many :issue_changes, :through => :issues, :source => :journals
44 44 has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy
45 45 has_many :time_entries, :dependent => :destroy
46 46 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
47 47 has_many :documents, :dependent => :destroy
48 48 has_many :news, lambda {includes(:author)}, :dependent => :destroy
49 49 has_many :issue_categories, lambda {order("#{IssueCategory.table_name}.name")}, :dependent => :delete_all
50 50 has_many :boards, lambda {order("position ASC")}, :dependent => :destroy
51 51 has_one :repository, lambda {where(["is_default = ?", true])}
52 52 has_many :repositories, :dependent => :destroy
53 53 has_many :changesets, :through => :repository
54 54 has_one :wiki, :dependent => :destroy
55 55 # Custom field for the project issues
56 56 has_and_belongs_to_many :issue_custom_fields,
57 57 lambda {order("#{CustomField.table_name}.position")},
58 58 :class_name => 'IssueCustomField',
59 59 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
60 60 :association_foreign_key => 'custom_field_id'
61 61
62 62 acts_as_attachable :view_permission => :view_files,
63 63 :edit_permission => :manage_files,
64 64 :delete_permission => :manage_files
65 65
66 66 acts_as_customizable
67 67 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
68 68 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
69 69 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
70 70 :author => nil
71 71
72 72 attr_protected :status
73 73
74 74 validates_presence_of :name, :identifier
75 75 validates_uniqueness_of :identifier, :if => Proc.new {|p| p.identifier_changed?}
76 76 validates_length_of :name, :maximum => 255
77 77 validates_length_of :homepage, :maximum => 255
78 78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
79 79 # downcase letters, digits, dashes but not digits only
80 80 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
81 81 # reserved words
82 82 validates_exclusion_of :identifier, :in => %w( new )
83 validate :validate_parent
83 84
84 85 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
85 86 after_save :remove_inherited_member_roles, :add_inherited_member_roles, :if => Proc.new {|project| project.parent_id_changed?}
87 after_update :update_versions_from_hierarchy_change, :if => Proc.new {|project| project.parent_id_changed?}
86 88 before_destroy :delete_all_members
87 89
88 90 scope :has_module, lambda {|mod|
89 91 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
90 92 }
91 93 scope :active, lambda { where(:status => STATUS_ACTIVE) }
92 94 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
93 95 scope :all_public, lambda { where(:is_public => true) }
94 96 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
95 97 scope :allowed_to, lambda {|*args|
96 98 user = User.current
97 99 permission = nil
98 100 if args.first.is_a?(Symbol)
99 101 permission = args.shift
100 102 else
101 103 user = args.shift
102 104 permission = args.shift
103 105 end
104 106 where(Project.allowed_to_condition(user, permission, *args))
105 107 }
106 108 scope :like, lambda {|arg|
107 109 if arg.blank?
108 110 where(nil)
109 111 else
110 112 pattern = "%#{arg.to_s.strip.downcase}%"
111 113 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
112 114 end
113 115 }
114 116 scope :sorted, lambda {order(:lft)}
115 117
116 118 def initialize(attributes=nil, *args)
117 119 super
118 120
119 121 initialized = (attributes || {}).stringify_keys
120 122 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
121 123 self.identifier = Project.next_identifier
122 124 end
123 125 if !initialized.key?('is_public')
124 126 self.is_public = Setting.default_projects_public?
125 127 end
126 128 if !initialized.key?('enabled_module_names')
127 129 self.enabled_module_names = Setting.default_projects_modules
128 130 end
129 131 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
130 132 default = Setting.default_projects_tracker_ids
131 133 if default.is_a?(Array)
132 134 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
133 135 else
134 136 self.trackers = Tracker.sorted.to_a
135 137 end
136 138 end
137 139 end
138 140
139 141 def identifier=(identifier)
140 142 super unless identifier_frozen?
141 143 end
142 144
143 145 def identifier_frozen?
144 146 errors[:identifier].blank? && !(new_record? || identifier.blank?)
145 147 end
146 148
147 149 # returns latest created projects
148 150 # non public projects will be returned only if user is a member of those
149 151 def self.latest(user=nil, count=5)
150 152 visible(user).limit(count).order("created_on DESC").to_a
151 153 end
152 154
153 155 # Returns true if the project is visible to +user+ or to the current user.
154 156 def visible?(user=User.current)
155 157 user.allowed_to?(:view_project, self)
156 158 end
157 159
158 160 # Returns a SQL conditions string used to find all projects visible by the specified user.
159 161 #
160 162 # Examples:
161 163 # Project.visible_condition(admin) => "projects.status = 1"
162 164 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
163 165 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
164 166 def self.visible_condition(user, options={})
165 167 allowed_to_condition(user, :view_project, options)
166 168 end
167 169
168 170 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
169 171 #
170 172 # Valid options:
171 173 # * :project => limit the condition to project
172 174 # * :with_subprojects => limit the condition to project and its subprojects
173 175 # * :member => limit the condition to the user projects
174 176 def self.allowed_to_condition(user, permission, options={})
175 177 perm = Redmine::AccessControl.permission(permission)
176 178 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
177 179 if perm && perm.project_module
178 180 # If the permission belongs to a project module, make sure the module is enabled
179 181 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
180 182 end
181 183 if project = options[:project]
182 184 project_statement = project.project_condition(options[:with_subprojects])
183 185 base_statement = "(#{project_statement}) AND (#{base_statement})"
184 186 end
185 187
186 188 if user.admin?
187 189 base_statement
188 190 else
189 191 statement_by_role = {}
190 192 unless options[:member]
191 193 role = user.builtin_role
192 194 if role.allowed_to?(permission)
193 195 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
194 196 end
195 197 end
196 198 user.projects_by_role.each do |role, projects|
197 199 if role.allowed_to?(permission) && projects.any?
198 200 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
199 201 end
200 202 end
201 203 if statement_by_role.empty?
202 204 "1=0"
203 205 else
204 206 if block_given?
205 207 statement_by_role.each do |role, statement|
206 208 if s = yield(role, user)
207 209 statement_by_role[role] = "(#{statement} AND (#{s}))"
208 210 end
209 211 end
210 212 end
211 213 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
212 214 end
213 215 end
214 216 end
215 217
216 218 def override_roles(role)
217 219 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
218 220 member = member_principals.where("#{Principal.table_name}.type = ?", group_class.name).first
219 221 member ? member.roles.to_a : [role]
220 222 end
221 223
222 224 def principals
223 225 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
224 226 end
225 227
226 228 def users
227 229 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
228 230 end
229 231
230 232 # Returns the Systemwide and project specific activities
231 233 def activities(include_inactive=false)
232 234 if include_inactive
233 235 return all_activities
234 236 else
235 237 return active_activities
236 238 end
237 239 end
238 240
239 241 # Will create a new Project specific Activity or update an existing one
240 242 #
241 243 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
242 244 # does not successfully save.
243 245 def update_or_create_time_entry_activity(id, activity_hash)
244 246 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
245 247 self.create_time_entry_activity_if_needed(activity_hash)
246 248 else
247 249 activity = project.time_entry_activities.find_by_id(id.to_i)
248 250 activity.update_attributes(activity_hash) if activity
249 251 end
250 252 end
251 253
252 254 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
253 255 #
254 256 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
255 257 # does not successfully save.
256 258 def create_time_entry_activity_if_needed(activity)
257 259 if activity['parent_id']
258 260 parent_activity = TimeEntryActivity.find(activity['parent_id'])
259 261 activity['name'] = parent_activity.name
260 262 activity['position'] = parent_activity.position
261 263 if Enumeration.overriding_change?(activity, parent_activity)
262 264 project_activity = self.time_entry_activities.create(activity)
263 265 if project_activity.new_record?
264 266 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
265 267 else
266 268 self.time_entries.
267 269 where(["activity_id = ?", parent_activity.id]).
268 270 update_all("activity_id = #{project_activity.id}")
269 271 end
270 272 end
271 273 end
272 274 end
273 275
274 276 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
275 277 #
276 278 # Examples:
277 279 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
278 280 # project.project_condition(false) => "projects.id = 1"
279 281 def project_condition(with_subprojects)
280 282 cond = "#{Project.table_name}.id = #{id}"
281 283 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
282 284 cond
283 285 end
284 286
285 287 def self.find(*args)
286 288 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
287 289 project = find_by_identifier(*args)
288 290 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
289 291 project
290 292 else
291 293 super
292 294 end
293 295 end
294 296
295 297 def self.find_by_param(*args)
296 298 self.find(*args)
297 299 end
298 300
299 301 alias :base_reload :reload
300 302 def reload(*args)
301 303 @principals = nil
302 304 @users = nil
303 305 @shared_versions = nil
304 306 @rolled_up_versions = nil
305 307 @rolled_up_trackers = nil
306 308 @all_issue_custom_fields = nil
307 309 @all_time_entry_custom_fields = nil
308 310 @to_param = nil
309 311 @allowed_parents = nil
310 312 @allowed_permissions = nil
311 313 @actions_allowed = nil
312 314 @start_date = nil
313 315 @due_date = nil
314 316 @override_members = nil
315 317 @assignable_users = nil
316 318 base_reload(*args)
317 319 end
318 320
319 321 def to_param
320 322 # id is used for projects with a numeric identifier (compatibility)
321 323 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
322 324 end
323 325
324 326 def active?
325 327 self.status == STATUS_ACTIVE
326 328 end
327 329
328 330 def archived?
329 331 self.status == STATUS_ARCHIVED
330 332 end
331 333
332 334 # Archives the project and its descendants
333 335 def archive
334 336 # Check that there is no issue of a non descendant project that is assigned
335 337 # to one of the project or descendant versions
336 338 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
337 339
338 340 if version_ids.any? &&
339 341 Issue.
340 342 includes(:project).
341 343 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
342 344 where(:fixed_version_id => version_ids).
343 345 exists?
344 346 return false
345 347 end
346 348 Project.transaction do
347 349 archive!
348 350 end
349 351 true
350 352 end
351 353
352 354 # Unarchives the project
353 355 # All its ancestors must be active
354 356 def unarchive
355 357 return false if ancestors.detect {|a| !a.active?}
356 358 update_attribute :status, STATUS_ACTIVE
357 359 end
358 360
359 361 def close
360 362 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
361 363 end
362 364
363 365 def reopen
364 366 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
365 367 end
366 368
367 369 # Returns an array of projects the project can be moved to
368 370 # by the current user
369 def allowed_parents
371 def allowed_parents(user=User.current)
370 372 return @allowed_parents if @allowed_parents
371 @allowed_parents = Project.allowed_to(User.current, :add_subprojects).to_a
373 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
372 374 @allowed_parents = @allowed_parents - self_and_descendants
373 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
375 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
374 376 @allowed_parents << nil
375 377 end
376 378 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
377 379 @allowed_parents << parent
378 380 end
379 381 @allowed_parents
380 382 end
381 383
382 384 # Sets the parent of the project with authorization check
383 385 def set_allowed_parent!(p)
384 unless p.nil? || p.is_a?(Project)
385 if p.to_s.blank?
386 p = nil
387 else
388 p = Project.find_by_id(p)
389 return false unless p
390 end
391 end
392 if p.nil?
393 if !new_record? && allowed_parents.empty?
394 return false
395 end
396 elsif !allowed_parents.include?(p)
397 return false
398 end
399 set_parent!(p)
386 p = p.id if p.is_a?(Project)
387 send :safe_attributes, {:project_id => p}
388 save
400 389 end
401 390
402 391 # Sets the parent of the project
403 392 # Argument can be either a Project, a String, a Fixnum or nil
404 393 def set_parent!(p)
405 unless p.nil? || p.is_a?(Project)
406 if p.to_s.blank?
407 p = nil
408 else
409 p = Project.find_by_id(p)
410 return false unless p
411 end
412 end
413 if p == parent && !p.nil?
414 # Nothing to do
415 true
416 elsif p.nil? || (p.active? && move_possible?(p))
394 if p.is_a?(Project)
417 395 self.parent = p
418 save
419 p.reload if p
420 Issue.update_versions_from_hierarchy_change(self)
421 true
422 396 else
423 # Can not move to the given target
424 false
397 self.parent_id = p
425 398 end
399 save
426 400 end
427 401
428 402 # Returns an array of the trackers used by the project and its active sub projects
429 403 def rolled_up_trackers
430 404 @rolled_up_trackers ||=
431 405 Tracker.
432 406 joins(:projects).
433 407 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
434 408 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED).
435 409 uniq.
436 410 sorted.
437 411 to_a
438 412 end
439 413
440 414 # Closes open and locked project versions that are completed
441 415 def close_completed_versions
442 416 Version.transaction do
443 417 versions.where(:status => %w(open locked)).each do |version|
444 418 if version.completed?
445 419 version.update_attribute(:status, 'closed')
446 420 end
447 421 end
448 422 end
449 423 end
450 424
451 425 # Returns a scope of the Versions on subprojects
452 426 def rolled_up_versions
453 427 @rolled_up_versions ||=
454 428 Version.
455 429 joins(:project).
456 430 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
457 431 end
458 432
459 433 # Returns a scope of the Versions used by the project
460 434 def shared_versions
461 435 if new_record?
462 436 Version.
463 437 joins(:project).
464 438 preload(:project).
465 439 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
466 440 else
467 441 @shared_versions ||= begin
468 442 r = root? ? self : root
469 443 Version.
470 444 joins(:project).
471 445 preload(:project).
472 446 where("#{Project.table_name}.id = #{id}" +
473 447 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
474 448 " #{Version.table_name}.sharing = 'system'" +
475 449 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
476 450 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
477 451 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
478 452 "))")
479 453 end
480 454 end
481 455 end
482 456
483 457 # Returns a hash of project users grouped by role
484 458 def users_by_role
485 459 members.includes(:user, :roles).inject({}) do |h, m|
486 460 m.roles.each do |r|
487 461 h[r] ||= []
488 462 h[r] << m.user
489 463 end
490 464 h
491 465 end
492 466 end
493 467
494 468 # Adds user as a project member with the default role
495 469 # Used for when a non-admin user creates a project
496 470 def add_default_member(user)
497 471 role = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
498 472 member = Member.new(:project => self, :principal => user, :roles => [role])
499 473 self.members << member
500 474 member
501 475 end
502 476
503 477 # Deletes all project's members
504 478 def delete_all_members
505 479 me, mr = Member.table_name, MemberRole.table_name
506 480 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
507 481 Member.delete_all(['project_id = ?', id])
508 482 end
509 483
510 484 # Return a Principal scope of users/groups issues can be assigned to
511 485 def assignable_users
512 486 types = ['User']
513 487 types << 'Group' if Setting.issue_group_assignment?
514 488
515 489 @assignable_users ||= Principal.
516 490 active.
517 491 joins(:members => :roles).
518 492 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
519 493 uniq.
520 494 sorted
521 495 end
522 496
523 497 # Returns the mail addresses of users that should be always notified on project events
524 498 def recipients
525 499 notified_users.collect {|user| user.mail}
526 500 end
527 501
528 502 # Returns the users that should be notified on project events
529 503 def notified_users
530 504 # TODO: User part should be extracted to User#notify_about?
531 505 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
532 506 end
533 507
534 508 # Returns a scope of all custom fields enabled for project issues
535 509 # (explicitly associated custom fields and custom fields enabled for all projects)
536 510 def all_issue_custom_fields
537 511 @all_issue_custom_fields ||= IssueCustomField.
538 512 sorted.
539 513 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
540 514 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
541 515 " WHERE cfp.project_id = ?)", true, id)
542 516 end
543 517
544 518 def project
545 519 self
546 520 end
547 521
548 522 def <=>(project)
549 523 name.downcase <=> project.name.downcase
550 524 end
551 525
552 526 def to_s
553 527 name
554 528 end
555 529
556 530 # Returns a short description of the projects (first lines)
557 531 def short_description(length = 255)
558 532 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
559 533 end
560 534
561 535 def css_classes
562 536 s = 'project'
563 537 s << ' root' if root?
564 538 s << ' child' if child?
565 539 s << (leaf? ? ' leaf' : ' parent')
566 540 unless active?
567 541 if archived?
568 542 s << ' archived'
569 543 else
570 544 s << ' closed'
571 545 end
572 546 end
573 547 s
574 548 end
575 549
576 550 # The earliest start date of a project, based on it's issues and versions
577 551 def start_date
578 552 @start_date ||= [
579 553 issues.minimum('start_date'),
580 554 shared_versions.minimum('effective_date'),
581 555 Issue.fixed_version(shared_versions).minimum('start_date')
582 556 ].compact.min
583 557 end
584 558
585 559 # The latest due date of an issue or version
586 560 def due_date
587 561 @due_date ||= [
588 562 issues.maximum('due_date'),
589 563 shared_versions.maximum('effective_date'),
590 564 Issue.fixed_version(shared_versions).maximum('due_date')
591 565 ].compact.max
592 566 end
593 567
594 568 def overdue?
595 569 active? && !due_date.nil? && (due_date < Date.today)
596 570 end
597 571
598 572 # Returns the percent completed for this project, based on the
599 573 # progress on it's versions.
600 574 def completed_percent(options={:include_subprojects => false})
601 575 if options.delete(:include_subprojects)
602 576 total = self_and_descendants.collect(&:completed_percent).sum
603 577
604 578 total / self_and_descendants.count
605 579 else
606 580 if versions.count > 0
607 581 total = versions.collect(&:completed_percent).sum
608 582
609 583 total / versions.count
610 584 else
611 585 100
612 586 end
613 587 end
614 588 end
615 589
616 590 # Return true if this project allows to do the specified action.
617 591 # action can be:
618 592 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
619 593 # * a permission Symbol (eg. :edit_project)
620 594 def allows_to?(action)
621 595 if archived?
622 596 # No action allowed on archived projects
623 597 return false
624 598 end
625 599 unless active? || Redmine::AccessControl.read_action?(action)
626 600 # No write action allowed on closed projects
627 601 return false
628 602 end
629 603 # No action allowed on disabled modules
630 604 if action.is_a? Hash
631 605 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
632 606 else
633 607 allowed_permissions.include? action
634 608 end
635 609 end
636 610
637 611 # Return the enabled module with the given name
638 612 # or nil if the module is not enabled for the project
639 613 def enabled_module(name)
640 614 name = name.to_s
641 615 enabled_modules.detect {|m| m.name == name}
642 616 end
643 617
644 618 # Return true if the module with the given name is enabled
645 619 def module_enabled?(name)
646 620 enabled_module(name).present?
647 621 end
648 622
649 623 def enabled_module_names=(module_names)
650 624 if module_names && module_names.is_a?(Array)
651 625 module_names = module_names.collect(&:to_s).reject(&:blank?)
652 626 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
653 627 else
654 628 enabled_modules.clear
655 629 end
656 630 end
657 631
658 632 # Returns an array of the enabled modules names
659 633 def enabled_module_names
660 634 enabled_modules.collect(&:name)
661 635 end
662 636
663 637 # Enable a specific module
664 638 #
665 639 # Examples:
666 640 # project.enable_module!(:issue_tracking)
667 641 # project.enable_module!("issue_tracking")
668 642 def enable_module!(name)
669 643 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
670 644 end
671 645
672 646 # Disable a module if it exists
673 647 #
674 648 # Examples:
675 649 # project.disable_module!(:issue_tracking)
676 650 # project.disable_module!("issue_tracking")
677 651 # project.disable_module!(project.enabled_modules.first)
678 652 def disable_module!(target)
679 653 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
680 654 target.destroy unless target.blank?
681 655 end
682 656
683 657 safe_attributes 'name',
684 658 'description',
685 659 'homepage',
686 660 'is_public',
687 661 'identifier',
688 662 'custom_field_values',
689 663 'custom_fields',
690 664 'tracker_ids',
691 'issue_custom_field_ids'
665 'issue_custom_field_ids',
666 'parent_id'
692 667
693 668 safe_attributes 'enabled_module_names',
694 669 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
695 670
696 671 safe_attributes 'inherit_members',
697 672 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
698 673
674 def safe_attributes=(attrs, user=User.current)
675 return unless attrs.is_a?(Hash)
676 attrs = attrs.deep_dup
677
678 @unallowed_parent_id = nil
679 parent_id_param = attrs['parent_id'].to_s
680 if parent_id_param.blank? || parent_id_param != parent_id.to_s
681 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
682 unless allowed_parents(user).include?(p)
683 attrs.delete('parent_id')
684 @unallowed_parent_id = true
685 end
686 end
687
688 super(attrs, user)
689 end
690
699 691 # Returns an auto-generated project identifier based on the last identifier used
700 692 def self.next_identifier
701 693 p = Project.order('id DESC').first
702 694 p.nil? ? nil : p.identifier.to_s.succ
703 695 end
704 696
705 697 # Copies and saves the Project instance based on the +project+.
706 698 # Duplicates the source project's:
707 699 # * Wiki
708 700 # * Versions
709 701 # * Categories
710 702 # * Issues
711 703 # * Members
712 704 # * Queries
713 705 #
714 706 # Accepts an +options+ argument to specify what to copy
715 707 #
716 708 # Examples:
717 709 # project.copy(1) # => copies everything
718 710 # project.copy(1, :only => 'members') # => copies members only
719 711 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
720 712 def copy(project, options={})
721 713 project = project.is_a?(Project) ? project : Project.find(project)
722 714
723 715 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
724 716 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
725 717
726 718 Project.transaction do
727 719 if save
728 720 reload
729 721 to_be_copied.each do |name|
730 722 send "copy_#{name}", project
731 723 end
732 724 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
733 725 save
734 726 end
735 727 end
736 728 true
737 729 end
738 730
739 731 # Returns a new unsaved Project instance with attributes copied from +project+
740 732 def self.copy_from(project)
741 733 project = project.is_a?(Project) ? project : Project.find(project)
742 734 # clear unique attributes
743 735 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
744 736 copy = Project.new(attributes)
745 737 copy.enabled_modules = project.enabled_modules
746 738 copy.trackers = project.trackers
747 739 copy.custom_values = project.custom_values.collect {|v| v.clone}
748 740 copy.issue_custom_fields = project.issue_custom_fields
749 741 copy
750 742 end
751 743
752 744 # Yields the given block for each project with its level in the tree
753 745 def self.project_tree(projects, &block)
754 746 ancestors = []
755 747 projects.sort_by(&:lft).each do |project|
756 748 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
757 749 ancestors.pop
758 750 end
759 751 yield project, ancestors.size
760 752 ancestors << project
761 753 end
762 754 end
763 755
764 756 private
765 757
766 758 def update_inherited_members
767 759 if parent
768 760 if inherit_members? && !inherit_members_was
769 761 remove_inherited_member_roles
770 762 add_inherited_member_roles
771 763 elsif !inherit_members? && inherit_members_was
772 764 remove_inherited_member_roles
773 765 end
774 766 end
775 767 end
776 768
777 769 def remove_inherited_member_roles
778 770 member_roles = memberships.map(&:member_roles).flatten
779 771 member_role_ids = member_roles.map(&:id)
780 772 member_roles.each do |member_role|
781 773 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
782 774 member_role.destroy
783 775 end
784 776 end
785 777 end
786 778
787 779 def add_inherited_member_roles
788 780 if inherit_members? && parent
789 781 parent.memberships.each do |parent_member|
790 782 member = Member.find_or_new(self.id, parent_member.user_id)
791 783 parent_member.member_roles.each do |parent_member_role|
792 784 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
793 785 end
794 786 member.save!
795 787 end
796 788 memberships.reset
797 789 end
798 790 end
799 791
792 def update_versions_from_hierarchy_change
793 Issue.update_versions_from_hierarchy_change(self)
794 end
795
796 def validate_parent
797 if @unallowed_parent_id
798 errors.add(:parent_id, :invalid)
799 elsif parent_id_changed?
800 unless parent.nil? || (parent.active? && move_possible?(parent))
801 errors.add(:parent_id, :invalid)
802 end
803 end
804 end
805
800 806 # Copies wiki from +project+
801 807 def copy_wiki(project)
802 808 # Check that the source project has a wiki first
803 809 unless project.wiki.nil?
804 810 wiki = self.wiki || Wiki.new
805 811 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
806 812 wiki_pages_map = {}
807 813 project.wiki.pages.each do |page|
808 814 # Skip pages without content
809 815 next if page.content.nil?
810 816 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
811 817 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
812 818 new_wiki_page.content = new_wiki_content
813 819 wiki.pages << new_wiki_page
814 820 wiki_pages_map[page.id] = new_wiki_page
815 821 end
816 822
817 823 self.wiki = wiki
818 824 wiki.save
819 825 # Reproduce page hierarchy
820 826 project.wiki.pages.each do |page|
821 827 if page.parent_id && wiki_pages_map[page.id]
822 828 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
823 829 wiki_pages_map[page.id].save
824 830 end
825 831 end
826 832 end
827 833 end
828 834
829 835 # Copies versions from +project+
830 836 def copy_versions(project)
831 837 project.versions.each do |version|
832 838 new_version = Version.new
833 839 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
834 840 self.versions << new_version
835 841 end
836 842 end
837 843
838 844 # Copies issue categories from +project+
839 845 def copy_issue_categories(project)
840 846 project.issue_categories.each do |issue_category|
841 847 new_issue_category = IssueCategory.new
842 848 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
843 849 self.issue_categories << new_issue_category
844 850 end
845 851 end
846 852
847 853 # Copies issues from +project+
848 854 def copy_issues(project)
849 855 # Stores the source issue id as a key and the copied issues as the
850 856 # value. Used to map the two together for issue relations.
851 857 issues_map = {}
852 858
853 859 # Store status and reopen locked/closed versions
854 860 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
855 861 version_statuses.each do |version, status|
856 862 version.update_attribute :status, 'open'
857 863 end
858 864
859 865 # Get issues sorted by root_id, lft so that parent issues
860 866 # get copied before their children
861 867 project.issues.reorder('root_id, lft').each do |issue|
862 868 new_issue = Issue.new
863 869 new_issue.copy_from(issue, :subtasks => false, :link => false)
864 870 new_issue.project = self
865 871 # Changing project resets the custom field values
866 872 # TODO: handle this in Issue#project=
867 873 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
868 874 # Reassign fixed_versions by name, since names are unique per project
869 875 if issue.fixed_version && issue.fixed_version.project == project
870 876 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
871 877 end
872 878 # Reassign the category by name, since names are unique per project
873 879 if issue.category
874 880 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
875 881 end
876 882 # Parent issue
877 883 if issue.parent_id
878 884 if copied_parent = issues_map[issue.parent_id]
879 885 new_issue.parent_issue_id = copied_parent.id
880 886 end
881 887 end
882 888
883 889 self.issues << new_issue
884 890 if new_issue.new_record?
885 891 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
886 892 else
887 893 issues_map[issue.id] = new_issue unless new_issue.new_record?
888 894 end
889 895 end
890 896
891 897 # Restore locked/closed version statuses
892 898 version_statuses.each do |version, status|
893 899 version.update_attribute :status, status
894 900 end
895 901
896 902 # Relations after in case issues related each other
897 903 project.issues.each do |issue|
898 904 new_issue = issues_map[issue.id]
899 905 unless new_issue
900 906 # Issue was not copied
901 907 next
902 908 end
903 909
904 910 # Relations
905 911 issue.relations_from.each do |source_relation|
906 912 new_issue_relation = IssueRelation.new
907 913 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
908 914 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
909 915 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
910 916 new_issue_relation.issue_to = source_relation.issue_to
911 917 end
912 918 new_issue.relations_from << new_issue_relation
913 919 end
914 920
915 921 issue.relations_to.each do |source_relation|
916 922 new_issue_relation = IssueRelation.new
917 923 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
918 924 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
919 925 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
920 926 new_issue_relation.issue_from = source_relation.issue_from
921 927 end
922 928 new_issue.relations_to << new_issue_relation
923 929 end
924 930 end
925 931 end
926 932
927 933 # Copies members from +project+
928 934 def copy_members(project)
929 935 # Copy users first, then groups to handle members with inherited and given roles
930 936 members_to_copy = []
931 937 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
932 938 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
933 939
934 940 members_to_copy.each do |member|
935 941 new_member = Member.new
936 942 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
937 943 # only copy non inherited roles
938 944 # inherited roles will be added when copying the group membership
939 945 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
940 946 next if role_ids.empty?
941 947 new_member.role_ids = role_ids
942 948 new_member.project = self
943 949 self.members << new_member
944 950 end
945 951 end
946 952
947 953 # Copies queries from +project+
948 954 def copy_queries(project)
949 955 project.queries.each do |query|
950 956 new_query = IssueQuery.new
951 957 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
952 958 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
953 959 new_query.project = self
954 960 new_query.user_id = query.user_id
955 961 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
956 962 self.queries << new_query
957 963 end
958 964 end
959 965
960 966 # Copies boards from +project+
961 967 def copy_boards(project)
962 968 project.boards.each do |board|
963 969 new_board = Board.new
964 970 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
965 971 new_board.project = self
966 972 self.boards << new_board
967 973 end
968 974 end
969 975
970 976 def allowed_permissions
971 977 @allowed_permissions ||= begin
972 978 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
973 979 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
974 980 end
975 981 end
976 982
977 983 def allowed_actions
978 984 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
979 985 end
980 986
981 987 # Returns all the active Systemwide and project specific activities
982 988 def active_activities
983 989 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
984 990
985 991 if overridden_activity_ids.empty?
986 992 return TimeEntryActivity.shared.active
987 993 else
988 994 return system_activities_and_project_overrides
989 995 end
990 996 end
991 997
992 998 # Returns all the Systemwide and project specific activities
993 999 # (inactive and active)
994 1000 def all_activities
995 1001 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
996 1002
997 1003 if overridden_activity_ids.empty?
998 1004 return TimeEntryActivity.shared
999 1005 else
1000 1006 return system_activities_and_project_overrides(true)
1001 1007 end
1002 1008 end
1003 1009
1004 1010 # Returns the systemwide active activities merged with the project specific overrides
1005 1011 def system_activities_and_project_overrides(include_inactive=false)
1006 1012 t = TimeEntryActivity.table_name
1007 1013 scope = TimeEntryActivity.where(
1008 1014 "(#{t}.project_id IS NULL AND #{t}.id NOT IN (?)) OR (#{t}.project_id = ?)",
1009 1015 time_entry_activities.map(&:parent_id), id
1010 1016 )
1011 1017 unless include_inactive
1012 1018 scope = scope.active
1013 1019 end
1014 1020 scope
1015 1021 end
1016 1022
1017 1023 # Archives subprojects recursively
1018 1024 def archive!
1019 1025 children.each do |subproject|
1020 1026 subproject.send :archive!
1021 1027 end
1022 1028 update_attribute :status, STATUS_ARCHIVED
1023 1029 end
1024 1030 end
@@ -1,233 +1,235
1 1 module ObjectHelpers
2 2 def User.generate!(attributes={})
3 3 @generated_user_login ||= 'user0'
4 4 @generated_user_login.succ!
5 5 user = User.new(attributes)
6 6 user.login = @generated_user_login.dup if user.login.blank?
7 7 user.mail = "#{@generated_user_login}@example.com" if user.mail.blank?
8 8 user.firstname = "Bob" if user.firstname.blank?
9 9 user.lastname = "Doe" if user.lastname.blank?
10 10 yield user if block_given?
11 11 user.save!
12 12 user
13 13 end
14 14
15 15 def User.add_to_project(user, project, roles=nil)
16 16 roles = Role.find(1) if roles.nil?
17 17 roles = [roles] if roles.is_a?(Role)
18 18 Member.create!(:principal => user, :project => project, :roles => roles)
19 19 end
20 20
21 21 def Group.generate!(attributes={})
22 22 @generated_group_name ||= 'Group 0'
23 23 @generated_group_name.succ!
24 24 group = Group.new(attributes)
25 25 group.name = @generated_group_name.dup if group.name.blank?
26 26 yield group if block_given?
27 27 group.save!
28 28 group
29 29 end
30 30
31 31 def Project.generate!(attributes={})
32 32 @generated_project_identifier ||= 'project-0000'
33 33 @generated_project_identifier.succ!
34 34 project = Project.new(attributes)
35 35 project.name = @generated_project_identifier.dup if project.name.blank?
36 36 project.identifier = @generated_project_identifier.dup if project.identifier.blank?
37 37 yield project if block_given?
38 38 project.save!
39 39 project
40 40 end
41 41
42 42 def Project.generate_with_parent!(parent, attributes={})
43 project = Project.generate!(attributes)
44 project.set_parent!(parent)
43 project = Project.generate!(attributes) do |p|
44 p.parent = parent
45 end
46 parent.reload if parent
45 47 project
46 48 end
47 49
48 50 def IssueStatus.generate!(attributes={})
49 51 @generated_status_name ||= 'Status 0'
50 52 @generated_status_name.succ!
51 53 status = IssueStatus.new(attributes)
52 54 status.name = @generated_status_name.dup if status.name.blank?
53 55 yield status if block_given?
54 56 status.save!
55 57 status
56 58 end
57 59
58 60 def Tracker.generate!(attributes={})
59 61 @generated_tracker_name ||= 'Tracker 0'
60 62 @generated_tracker_name.succ!
61 63 tracker = Tracker.new(attributes)
62 64 tracker.name = @generated_tracker_name.dup if tracker.name.blank?
63 65 tracker.default_status ||= IssueStatus.order('position').first || IssueStatus.generate!
64 66 yield tracker if block_given?
65 67 tracker.save!
66 68 tracker
67 69 end
68 70
69 71 def Role.generate!(attributes={})
70 72 @generated_role_name ||= 'Role 0'
71 73 @generated_role_name.succ!
72 74 role = Role.new(attributes)
73 75 role.name = @generated_role_name.dup if role.name.blank?
74 76 yield role if block_given?
75 77 role.save!
76 78 role
77 79 end
78 80
79 81 # Generates an unsaved Issue
80 82 def Issue.generate(attributes={})
81 83 issue = Issue.new(attributes)
82 84 issue.project ||= Project.find(1)
83 85 issue.tracker ||= issue.project.trackers.first
84 86 issue.subject = 'Generated' if issue.subject.blank?
85 87 issue.author ||= User.find(2)
86 88 yield issue if block_given?
87 89 issue
88 90 end
89 91
90 92 # Generates a saved Issue
91 93 def Issue.generate!(attributes={}, &block)
92 94 issue = Issue.generate(attributes, &block)
93 95 issue.save!
94 96 issue
95 97 end
96 98
97 99 # Generates an issue with 2 children and a grandchild
98 100 def Issue.generate_with_descendants!(attributes={})
99 101 issue = Issue.generate!(attributes)
100 102 child = Issue.generate!(:project => issue.project, :subject => 'Child1', :parent_issue_id => issue.id)
101 103 Issue.generate!(:project => issue.project, :subject => 'Child2', :parent_issue_id => issue.id)
102 104 Issue.generate!(:project => issue.project, :subject => 'Child11', :parent_issue_id => child.id)
103 105 issue.reload
104 106 end
105 107
106 108 def Journal.generate!(attributes={})
107 109 journal = Journal.new(attributes)
108 110 journal.user ||= User.first
109 111 journal.journalized ||= Issue.first
110 112 yield journal if block_given?
111 113 journal.save!
112 114 journal
113 115 end
114 116
115 117 def Version.generate!(attributes={})
116 118 @generated_version_name ||= 'Version 0'
117 119 @generated_version_name.succ!
118 120 version = Version.new(attributes)
119 121 version.name = @generated_version_name.dup if version.name.blank?
120 122 yield version if block_given?
121 123 version.save!
122 124 version
123 125 end
124 126
125 127 def TimeEntry.generate!(attributes={})
126 128 entry = TimeEntry.new(attributes)
127 129 entry.user ||= User.find(2)
128 130 entry.issue ||= Issue.find(1) unless entry.project
129 131 entry.project ||= entry.issue.project
130 132 entry.activity ||= TimeEntryActivity.first
131 133 entry.spent_on ||= Date.today
132 134 entry.hours ||= 1.0
133 135 entry.save!
134 136 entry
135 137 end
136 138
137 139 def AuthSource.generate!(attributes={})
138 140 @generated_auth_source_name ||= 'Auth 0'
139 141 @generated_auth_source_name.succ!
140 142 source = AuthSource.new(attributes)
141 143 source.name = @generated_auth_source_name.dup if source.name.blank?
142 144 yield source if block_given?
143 145 source.save!
144 146 source
145 147 end
146 148
147 149 def Board.generate!(attributes={})
148 150 @generated_board_name ||= 'Forum 0'
149 151 @generated_board_name.succ!
150 152 board = Board.new(attributes)
151 153 board.name = @generated_board_name.dup if board.name.blank?
152 154 board.description = @generated_board_name.dup if board.description.blank?
153 155 yield board if block_given?
154 156 board.save!
155 157 board
156 158 end
157 159
158 160 def Attachment.generate!(attributes={})
159 161 @generated_filename ||= 'testfile0'
160 162 @generated_filename.succ!
161 163 attributes = attributes.dup
162 164 attachment = Attachment.new(attributes)
163 165 attachment.container ||= Issue.find(1)
164 166 attachment.author ||= User.find(2)
165 167 attachment.filename = @generated_filename.dup if attachment.filename.blank?
166 168 attachment.save!
167 169 attachment
168 170 end
169 171
170 172 def CustomField.generate!(attributes={})
171 173 @generated_custom_field_name ||= 'Custom field 0'
172 174 @generated_custom_field_name.succ!
173 175 field = new(attributes)
174 176 field.name = @generated_custom_field_name.dup if field.name.blank?
175 177 field.field_format = 'string' if field.field_format.blank?
176 178 yield field if block_given?
177 179 field.save!
178 180 field
179 181 end
180 182
181 183 def Changeset.generate!(attributes={})
182 184 @generated_changeset_rev ||= '123456'
183 185 @generated_changeset_rev.succ!
184 186 changeset = new(attributes)
185 187 changeset.repository ||= Project.find(1).repository
186 188 changeset.revision ||= @generated_changeset_rev
187 189 changeset.committed_on ||= Time.now
188 190 yield changeset if block_given?
189 191 changeset.save!
190 192 changeset
191 193 end
192 194
193 195 def Query.generate!(attributes={})
194 196 query = new(attributes)
195 197 query.name = "Generated query" if query.name.blank?
196 198 query.user ||= User.find(1)
197 199 query.save!
198 200 query
199 201 end
200 202 end
201 203
202 204 module TrackerObjectHelpers
203 205 def generate_transitions!(*args)
204 206 options = args.last.is_a?(Hash) ? args.pop : {}
205 207 if args.size == 1
206 208 args << args.first
207 209 end
208 210 if options[:clear]
209 211 WorkflowTransition.where(:tracker_id => id).delete_all
210 212 end
211 213 args.each_cons(2) do |old_status_id, new_status_id|
212 214 WorkflowTransition.create!(
213 215 :tracker => self,
214 216 :role_id => (options[:role_id] || 1),
215 217 :old_status_id => old_status_id,
216 218 :new_status_id => new_status_id
217 219 )
218 220 end
219 221 end
220 222 end
221 223 Tracker.send :include, TrackerObjectHelpers
222 224
223 225 module IssueObjectHelpers
224 226 def close!
225 227 self.status = IssueStatus.where(:is_closed => true).first
226 228 save!
227 229 end
228 230
229 231 def generate_child!(attributes={})
230 232 Issue.generate!(attributes.merge(:parent_issue_id => self.id))
231 233 end
232 234 end
233 235 Issue.send :include, IssueObjectHelpers
General Comments 0
You need to be logged in to leave comments. Login now