Class: Project

Inherits:
ActiveRecord::Base show all
Defined in:
app/models/project.rb,
vendor/plugins/classic_pagination/test/fixtures/project.rb

Overview

redMine - project management software Copyright (C) 2006 Jean-Philippe Lang

This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

Constant Summary

STATUS_ACTIVE =

Project statuses

1
STATUS_ARCHIVED =
9

Class Method Summary

Instance Method Summary

Methods inherited from ActiveRecord::Base

quoted_table_name

Class Method Details

+ (Object) allowed_to_condition(user, permission, options = {})



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'app/models/project.rb', line 114

def self.allowed_to_condition(user, permission, options={})
  statements = []
  base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
  if perm = Redmine::AccessControl.permission(permission)
    unless perm.project_module.nil?
      # If the permission belongs to a project module, make sure the module is enabled
      base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
    end
  end
  if options[:project]
    project_statement = "#{Project.table_name}.id = #{options[:project].id}"
    project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
    base_statement = "(#{project_statement}) AND (#{base_statement})"
  end
  if user.admin?
    # no restriction
  else
    statements << "1=0"
    if user.logged?
      if Role.non_member.allowed_to?(permission) && !options[:member]
        statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
      end
      allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
      statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
    else
      if Role.anonymous.allowed_to?(permission) && !options[:member]
        # anonymous user allowed on public project
        statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
      end 
    end
  end
  statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
end

+ (Object) copy_from(project)

Copies project and returns the new instance. This will not save the copy



480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
# File 'app/models/project.rb', line 480

def self.copy_from(project)
  begin
    project = project.is_a?(Project) ? project : Project.find(project)
    if project
      # clear unique attributes
      attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
      copy = Project.new(attributes)
      copy.enabled_modules = project.enabled_modules
      copy.trackers = project.trackers
      copy.custom_values = project.custom_values.collect {|v| v.clone}
      copy.issue_custom_fields = project.issue_custom_fields
      return copy
    else
      return nil
    end
  rescue ActiveRecord::RecordNotFound
    return nil
  end
end

+ (Object) find(*args)



204
205
206
207
208
209
210
211
212
# File 'app/models/project.rb', line 204

def self.find(*args)
  if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
    project = find_by_identifier(*args)
    raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
    project
  else
    super
  end
end

+ (Object) latest(user = nil, count = 5)

returns latest created projects non public projects will be returned only if user is a member of those



94
95
96
# File 'app/models/project.rb', line 94

def self.latest(user=nil, count=5)
  find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC") 
end

+ (Object) next_identifier

Returns an auto-generated project identifier based on the last identifier used



439
440
441
442
# File 'app/models/project.rb', line 439

def self.next_identifier
  p = Project.find(:first, :order => 'created_on DESC')
  p.nil? ? nil : p.identifier.to_s.succ
end

+ (Object) visible_by(user = nil)

Returns a SQL :conditions string used to find all active projects for the specified user.

Examples:

    Projects.visible_by(admin)        => "projects.status = 1"
    Projects.visible_by(normal_user)  => "projects.status = 1 AND projects.is_public = 1"


103
104
105
106
107
108
109
110
111
112
# File 'app/models/project.rb', line 103

def self.visible_by(user=nil)
  user ||= User.current
  if user && user.admin?
    return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
  elsif user && user.memberships.any?
    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(',')}))"
  else
    return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
  end
end

Instance Method Details

- (Object) <=>(project)



396
397
398
# File 'app/models/project.rb', line 396

def <=>(project)
  name.downcase <=> project.name.downcase
end

- (Boolean) active?

Returns:

  • (Boolean)


219
220
221
# File 'app/models/project.rb', line 219

def active?
  self.status == STATUS_ACTIVE
end

- (Object) activities(include_inactive = false)

Returns the Systemwide and project specific activities



149
150
151
152
153
154
155
# File 'app/models/project.rb', line 149

def activities(include_inactive=false)
  if include_inactive
    return all_activities
  else
    return active_activities
  end
end

- (Object) all_issue_custom_fields

Returns an array of all custom fields enabled for project issues (explictly associated custom fields and custom fields enabled for all projects)



388
389
390
# File 'app/models/project.rb', line 388

def all_issue_custom_fields
  @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
end

- (Object) allowed_parents

Returns an array of projects the project can be moved to by the current user



248
249
250
251
252
253
254
255
256
257
258
259
# File 'app/models/project.rb', line 248

def allowed_parents
  return @allowed_parents if @allowed_parents
  @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
  @allowed_parents = @allowed_parents - self_and_descendants
  if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
    @allowed_parents << nil
  end
  unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
    @allowed_parents << parent
  end
  @allowed_parents
end

- (Boolean) allows_to?(action)

Return true if this project is allowed to do the specified action. action can be:

  • a parameter-like Hash (eg. :controller => ‘projects’, :action => ‘edit’)
  • a permission Symbol (eg. :edit_project)

Returns:

  • (Boolean)


413
414
415
416
417
418
419
# File 'app/models/project.rb', line 413

def allows_to?(action)
  if action.is_a? Hash
    allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
  else
    allowed_permissions.include? action
  end
end

- (Object) archive

Archives the project and its descendants



224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'app/models/project.rb', line 224

def archive
  # Check that there is no issue of a non descendant project that is assigned
  # to one of the project or descendant versions
  v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
  if v_ids.any? && Issue.find(:first, :include => :project,
                                      :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
                                                      " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
    return false
  end
  Project.transaction do
    archive!
  end
  true
end

- (Object) assignable_users

Users issues can be assigned to



372
373
374
# File 'app/models/project.rb', line 372

def assignable_users
  members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
end

- (Object) close_completed_versions

Closes open and locked project versions that are completed



330
331
332
333
334
335
336
337
338
# File 'app/models/project.rb', line 330

def close_completed_versions
  Version.transaction do
    versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
      if version.completed?
        version.update_attribute(:status, 'closed')
      end
    end
  end
end

- (Object) copy(project, options = {})

Copies and saves the Project instance based on the project. Duplicates the source project’s:

  • Wiki
  • Versions
  • Categories
  • Issues
  • Members
  • Queries

Accepts an options argument to specify what to copy

Examples:

  project.copy(1)                                    # => copies everything
  project.copy(1, :only => 'members')                # => copies members only
  project.copy(1, :only => ['members', 'versions'])  # => copies members and versions


459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'app/models/project.rb', line 459

def copy(project, options={})
  project = project.is_a?(Project) ? project : Project.find(project)
  
  to_be_copied = %w(wiki versions issue_categories issues members queries boards)
  to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
  
  Project.transaction do
    if save
      reload
      to_be_copied.each do |name|
        send "copy_#{name}", project
      end
      Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
      save
    end
  end
end

- (Object) create_time_entry_activity_if_needed(activity)

Create a new TimeEntryActivity if it overrides a system TimeEntryActivity

This will raise a ActiveRecord::Rollback if the TimeEntryActivity does not successfully save.



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'app/models/project.rb', line 174

def create_time_entry_activity_if_needed(activity)
  if activity['parent_id']
  
    parent_activity = TimeEntryActivity.find(activity['parent_id'])
    activity['name'] = parent_activity.name
    activity['position'] = parent_activity.position

    if Enumeration.overridding_change?(activity, parent_activity)
      project_activity = self.time_entry_activities.create(activity)

      if project_activity.new_record?
        raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
      else
        self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
      end
    end
  end
end

- (Object) delete_all_members

Deletes all project’s members



365
366
367
368
369
# File 'app/models/project.rb', line 365

def delete_all_members
  me, mr = Member.table_name, MemberRole.table_name
  connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
  Member.delete_all(['project_id = ?', id])
end

- (Object) enabled_module_names=(module_names)



426
427
428
429
430
431
432
433
434
435
436
# File 'app/models/project.rb', line 426

def enabled_module_names=(module_names)
  if module_names && module_names.is_a?(Array)
    module_names = module_names.collect(&:to_s)
    # remove disabled modules
    enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
    # add new modules
    module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
  else
    enabled_modules.clear
  end
end

- (Object) identifier=(identifier)



84
85
86
# File 'app/models/project.rb', line 84

def identifier=(identifier)
  super unless identifier_frozen?
end

- (Boolean) identifier_frozen?

Returns:

  • (Boolean)


88
89
90
# File 'app/models/project.rb', line 88

def identifier_frozen?
  errors[:identifier].nil? && !(new_record? || identifier.blank?)
end

- (Boolean) module_enabled?(module_name)

Returns:

  • (Boolean)


421
422
423
424
# File 'app/models/project.rb', line 421

def module_enabled?(module_name)
  module_name = module_name.to_s
  enabled_modules.detect {|m| m.name == module_name}
end

- (Object) notified_users

Returns the users that should be notified on project events



382
383
384
# File 'app/models/project.rb', line 382

def notified_users
  members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
end

- (Object) project



392
393
394
# File 'app/models/project.rb', line 392

def project
  self
end

- (Object) project_condition(with_subprojects)

Returns a :conditions SQL string that can be used to find the issues associated with this project.

Examples:

  project.project_condition(true)  => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
  project.project_condition(false) => "projects.id = 1"


198
199
200
201
202
# File 'app/models/project.rb', line 198

def project_condition(with_subprojects)
  cond = "#{Project.table_name}.id = #{id}"
  cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
  cond
end

- (Object) recipients

Returns the mail adresses of users that should be always notified on project events



377
378
379
# File 'app/models/project.rb', line 377

def recipients
  members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
end

- (Object) rolled_up_trackers

Returns an array of the trackers used by the project and its active sub projects



321
322
323
324
325
326
327
# File 'app/models/project.rb', line 321

def rolled_up_trackers
  @rolled_up_trackers ||=
    Tracker.find(:all, :include => :projects,
                       :select => "DISTINCT #{Tracker.table_name}.*",
                       :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
                       :order => "#{Tracker.table_name}.position")
end

- (Object) set_allowed_parent!(p)

Sets the parent of the project with authorization check



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'app/models/project.rb', line 262

def set_allowed_parent!(p)
  unless p.nil? || p.is_a?(Project)
    if p.to_s.blank?
      p = nil
    else
      p = Project.find_by_id(p)
      return false unless p
    end
  end
  if p.nil?
    if !new_record? && allowed_parents.empty?
      return false
    end
  elsif !allowed_parents.include?(p)
    return false
  end
  set_parent!(p)
end

- (Object) set_parent!(p)

Sets the parent of the project Argument can be either a Project, a String, a Fixnum or nil



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'app/models/project.rb', line 283

def set_parent!(p)
  unless p.nil? || p.is_a?(Project)
    if p.to_s.blank?
      p = nil
    else
      p = Project.find_by_id(p)
      return false unless p
    end
  end
  if p == parent && !p.nil?
    # Nothing to do
    true
  elsif p.nil? || (p.active? && move_possible?(p))
    # Insert the project so that target's children or root projects stay alphabetically sorted
    sibs = (p.nil? ? self.class.roots : p.children)
    to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
    if to_be_inserted_before
      move_to_left_of(to_be_inserted_before)
    elsif p.nil?
      if sibs.empty?
        # move_to_root adds the project in first (ie. left) position
        move_to_root
      else
        move_to_right_of(sibs.last) unless self == sibs.last
      end
    else
      # move_to_child_of adds the project in last (ie.right) position
      move_to_child_of(p)
    end
    Issue.update_versions_from_hierarchy_change(self)
    true
  else
    # Can not move to the given target
    false
  end
end

- (Object) shared_versions

Returns a scope of the Versions used by the project



341
342
343
344
345
346
347
348
349
350
351
# File 'app/models/project.rb', line 341

def shared_versions
  @shared_versions ||= 
    Version.scoped(:include => :project,
                   :conditions => "#{Project.table_name}.id = #{id}" +
                                  " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
                                        " #{Version.table_name}.sharing = 'system'" +
                                        " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
                                        " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
                                        " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
                                        "))")
end

- (Object) short_description(length = 255)

Returns a short description of the projects (first lines)



405
406
407
# File 'app/models/project.rb', line 405

def short_description(length = 255)
  description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
end

- (Object) to_param



214
215
216
217
# File 'app/models/project.rb', line 214

def to_param
  # id is used for projects with a numeric identifier (compatibility)
  @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
end

- (Object) to_s



400
401
402
# File 'app/models/project.rb', line 400

def to_s
  name
end

- (Object) unarchive

Unarchives the project All its ancestors must be active



241
242
243
244
# File 'app/models/project.rb', line 241

def unarchive
  return false if ancestors.detect {|a| !a.active?}
  update_attribute :status, STATUS_ACTIVE
end

- (Object) update_or_create_time_entry_activity(id, activity_hash)

Will create a new Project specific Activity or update an existing one

This will raise a ActiveRecord::Rollback if the TimeEntryActivity does not successfully save.



161
162
163
164
165
166
167
168
# File 'app/models/project.rb', line 161

def update_or_create_time_entry_activity(id, activity_hash)
  if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
    self.create_time_entry_activity_if_needed(activity_hash)
  else
    activity = project.time_entry_activities.find_by_id(id.to_i)
    activity.update_attributes(activity_hash) if activity
  end
end

- (Object) users_by_role

Returns a hash of project users grouped by role



354
355
356
357
358
359
360
361
362
# File 'app/models/project.rb', line 354

def users_by_role
  members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
    m.roles.each do |r|
      h[r] ||= []
      h[r] << m.user
    end
    h
  end
end