Class: Project

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
Redmine::SafeAttributes
Defined in:
app/models/project.rb

Overview

Redmine - project management software Copyright (C) 2006-2011 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
IDENTIFIER_MAX_LENGTH =

Maximum length for project identifiers

100

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Redmine::SafeAttributes

#delete_unsafe_attributes, included, #safe_attribute_names, #safe_attributes=

Constructor Details

#initialize(attributes = nil) ⇒ Project

Returns a new instance of Project



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'app/models/project.rb', line 89

def initialize(attributes = nil)
  super
  
  initialized = (attributes || {}).stringify_keys
  if !initialized.key?('identifier') && Setting.sequential_project_identifiers? 
    self.identifier = Project.next_identifier
  end
  if !initialized.key?('is_public')
    self.is_public = Setting.default_projects_public?
  end
  if !initialized.key?('enabled_module_names')
    self.enabled_module_names = Setting.default_projects_modules
  end
  if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
    self.trackers = Tracker.all
  end
end

Class Method Details

.allowed_to_condition(user, permission, options = {}) ⇒ Object



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'app/models/project.rb', line 137

def self.allowed_to_condition(user, permission, options={})
  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?
    base_statement
  else
    statement_by_role = {}
    if user.logged?
      if Role.non_member.allowed_to?(permission) && !options[:member]
        statement_by_role[Role.non_member] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
      end
      user.projects_by_role.each do |role, projects|
        if role.allowed_to?(permission)
          statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
        end
      end
    else
      if Role.anonymous.allowed_to?(permission) && !options[:member]
        statement_by_role[Role.anonymous] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
      end 
    end
    if statement_by_role.empty?
      "1=0"
    else
      "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
    end
  end
end

.copy_from(project) ⇒ Object

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



598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
# File 'app/models/project.rb', line 598

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

.find(*args) ⇒ Object



233
234
235
236
237
238
239
240
241
# File 'app/models/project.rb', line 233

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

.latest(user = nil, count = 5) ⇒ Object

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



117
118
119
# File 'app/models/project.rb', line 117

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

.next_identifierObject

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



557
558
559
560
# File 'app/models/project.rb', line 557

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

.project_tree(projects, &block) ⇒ Object

Yields the given block for each project with its level in the tree



619
620
621
622
623
624
625
626
627
628
# File 'app/models/project.rb', line 619

def self.project_tree(projects, &block)
  ancestors = []
  projects.sort_by(&:lft).each do |project|
    while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) 
      ancestors.pop
    end
    yield project, ancestors.size
    ancestors << project
  end
end

.visible_by(user = nil) ⇒ Object

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"


126
127
128
129
130
131
132
133
134
135
# File 'app/models/project.rb', line 126

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

#<=>(project) ⇒ Object



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

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

#active?Boolean

Returns:

  • (Boolean)


248
249
250
# File 'app/models/project.rb', line 248

def active?
  self.status == STATUS_ACTIVE
end

#activities(include_inactive = false) ⇒ Object

Returns the Systemwide and project specific activities



178
179
180
181
182
183
184
# File 'app/models/project.rb', line 178

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

#all_issue_custom_fieldsObject

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



431
432
433
# File 'app/models/project.rb', line 431

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

#allowed_parentsObject

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



281
282
283
284
285
286
287
288
289
290
291
292
# File 'app/models/project.rb', line 281

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

#allows_to?(action) ⇒ Boolean

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)


504
505
506
507
508
509
510
# File 'app/models/project.rb', line 504

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

#archiveObject

Archives the project and its descendants



257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'app/models/project.rb', line 257

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

#archived?Boolean

Returns:

  • (Boolean)


252
253
254
# File 'app/models/project.rb', line 252

def archived?
  self.status == STATUS_ARCHIVED
end

#assignable_usersObject

Users issues can be assigned to



414
415
416
# File 'app/models/project.rb', line 414

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

#close_completed_versionsObject

Closes open and locked project versions that are completed



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

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

#completed_percent(options = {:include_subprojects => false}) ⇒ Object

Returns the percent completed for this project, based on the progress on it's versions.



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

def completed_percent(options={:include_subprojects => false})
  if options.delete(:include_subprojects)
    total = self_and_descendants.collect(&:completed_percent).sum

    total / self_and_descendants.count
  else
    if versions.count > 0
      total = versions.collect(&:completed_pourcent).sum

      total / versions.count
    else
      100
    end
  end
end

#copy(project, options = {}) ⇒ Object

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


577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
# File 'app/models/project.rb', line 577

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

#create_time_entry_activity_if_needed(activity) ⇒ Object

Create a new TimeEntryActivity if it overrides a system TimeEntryActivity

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



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'app/models/project.rb', line 203

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

#css_classesObject



452
453
454
455
456
457
458
# File 'app/models/project.rb', line 452

def css_classes
  s = 'project'
  s << ' root' if root?
  s << ' child' if child?
  s << (leaf? ? ' leaf' : ' parent')
  s
end

#delete_all_membersObject

Deletes all project's members



407
408
409
410
411
# File 'app/models/project.rb', line 407

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

#due_dateObject

The latest due date of an issue or version



470
471
472
473
474
475
476
# File 'app/models/project.rb', line 470

def due_date
  [
   issues.maximum('due_date'),
   shared_versions.collect(&:effective_date),
   shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
  ].flatten.compact.max
end

#enabled_module_namesObject

Returns an array of the enabled modules names



530
531
532
# File 'app/models/project.rb', line 530

def enabled_module_names
  enabled_modules.collect(&:name)
end

#enabled_module_names=(module_names) ⇒ Object



517
518
519
520
521
522
523
524
525
526
527
# File 'app/models/project.rb', line 517

def enabled_module_names=(module_names)
  if module_names && module_names.is_a?(Array)
    module_names = module_names.collect(&:to_s).reject(&:blank?)
    # 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

#hierarchyObject

Returns an array of projects that are in this project's hierarchy

Example: parents, children, siblings



550
551
552
553
554
# File 'app/models/project.rb', line 550

def hierarchy
  parents = project.self_and_ancestors || []
  descendants = project.descendants || []
  project_hierarchy = parents | descendants # Set union
end

#identifier=(identifier) ⇒ Object



107
108
109
# File 'app/models/project.rb', line 107

def identifier=(identifier)
  super unless identifier_frozen?
end

#identifier_frozen?Boolean

Returns:

  • (Boolean)


111
112
113
# File 'app/models/project.rb', line 111

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

#module_enabled?(module_name) ⇒ Boolean

Returns:

  • (Boolean)


512
513
514
515
# File 'app/models/project.rb', line 512

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

#notified_usersObject

Returns the users that should be notified on project events



424
425
426
427
# File 'app/models/project.rb', line 424

def notified_users
  # TODO: User part should be extracted to User#notify_about?
  members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
end

#overdue?Boolean

Returns:

  • (Boolean)


478
479
480
# File 'app/models/project.rb', line 478

def overdue?
  active? && !due_date.nil? && (due_date < Date.today)
end

#projectObject



435
436
437
# File 'app/models/project.rb', line 435

def project
  self
end

#project_condition(with_subprojects) ⇒ Object

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"


227
228
229
230
231
# File 'app/models/project.rb', line 227

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

#recipientsObject

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



419
420
421
# File 'app/models/project.rb', line 419

def recipients
  notified_users.collect {|user| user.mail}
end

#rolled_up_trackersObject

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



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

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

#rolled_up_versionsObject

Returns a scope of the Versions on subprojects



374
375
376
377
378
# File 'app/models/project.rb', line 374

def rolled_up_versions
  @rolled_up_versions ||=
    Version.scoped(:include => :project,
                   :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
end

#set_allowed_parent!(p) ⇒ Object

Sets the parent of the project with authorization check



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'app/models/project.rb', line 295

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

#set_parent!(p) ⇒ Object

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



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'app/models/project.rb', line 316

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

#shared_versionsObject

Returns a scope of the Versions used by the project



381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'app/models/project.rb', line 381

def shared_versions
  @shared_versions ||= begin
    r = root? ? self : root
    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 >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.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
end

#short_description(length = 255) ⇒ Object

Returns a short description of the projects (first lines)



448
449
450
# File 'app/models/project.rb', line 448

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

#start_dateObject

The earliest start date of a project, based on it's issues and versions



461
462
463
464
465
466
467
# File 'app/models/project.rb', line 461

def start_date
  [
   issues.minimum('start_date'),
   shared_versions.collect(&:effective_date),
   shared_versions.collect(&:start_date)
  ].flatten.compact.min
end

#to_paramObject



243
244
245
246
# File 'app/models/project.rb', line 243

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

#to_sObject



443
444
445
# File 'app/models/project.rb', line 443

def to_s
  name
end

#unarchiveObject

Unarchives the project All its ancestors must be active



274
275
276
277
# File 'app/models/project.rb', line 274

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

#update_or_create_time_entry_activity(id, activity_hash) ⇒ Object

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.



190
191
192
193
194
195
196
197
# File 'app/models/project.rb', line 190

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

#users_by_roleObject

Returns a hash of project users grouped by role



396
397
398
399
400
401
402
403
404
# File 'app/models/project.rb', line 396

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