Class: Project

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

Overview

– copyright ChiliProject is a project management system.

Copyright (C) 2010-2013 the ChiliProject Team

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.

See doc/COPYRIGHT.rdoc for more details. ++

Constant Summary collapse

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


96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'app/models/project.rb', line 96

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


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
176
177
178
179
180
181
182
183
184
185
186
187
# File 'app/models/project.rb', line 149

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


607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
# File 'app/models/project.rb', line 607

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


245
246
247
248
249
250
251
252
253
# File 'app/models/project.rb', line 245

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


124
125
126
# File 'app/models/project.rb', line 124

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


566
567
568
569
# File 'app/models/project.rb', line 566

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


628
629
630
631
632
633
634
635
636
637
# File 'app/models/project.rb', line 628

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 if block_given?
    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"

133
134
135
136
137
138
139
140
141
142
# File 'app/models/project.rb', line 133

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


451
452
453
# File 'app/models/project.rb', line 451

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

#active?Boolean

Returns:

  • (Boolean)

260
261
262
# File 'app/models/project.rb', line 260

def active?
  self.status == STATUS_ACTIVE
end

#activities(include_inactive = false) ⇒ Object

Returns the Systemwide and project specific activities


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

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)


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

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


293
294
295
296
297
298
299
300
301
302
303
304
# File 'app/models/project.rb', line 293

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)

516
517
518
519
520
521
522
# File 'app/models/project.rb', line 516

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


269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'app/models/project.rb', line 269

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)

264
265
266
# File 'app/models/project.rb', line 264

def archived?
  self.status == STATUS_ARCHIVED
end

#assignable_usersObject

Users issues can be assigned to


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

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


375
376
377
378
379
380
381
382
383
# File 'app/models/project.rb', line 375

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.


496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'app/models/project.rb', line 496

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

586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
# File 'app/models/project.rb', line 586

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.


215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'app/models/project.rb', line 215

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


464
465
466
467
468
469
470
# File 'app/models/project.rb', line 464

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


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

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


482
483
484
485
486
487
488
# File 'app/models/project.rb', line 482

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


539
540
541
# File 'app/models/project.rb', line 539

def enabled_module_names
  enabled_modules.collect(&:name)
end

#enabled_module_names=(module_names) ⇒ Object


529
530
531
532
533
534
535
536
# File 'app/models/project.rb', line 529

def enabled_module_names=(module_names)
  if module_names && module_names.is_a?(Array)
    module_names = module_names.collect(&:to_s).reject(&:blank?)
    self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || 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


559
560
561
562
563
# File 'app/models/project.rb', line 559

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

#identifier=(identifier) ⇒ Object


114
115
116
# File 'app/models/project.rb', line 114

def identifier=(identifier)
  super unless identifier_frozen?
end

#identifier_frozen?Boolean

Returns:

  • (Boolean)

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

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

#module_enabled?(module_name) ⇒ Boolean

Returns:

  • (Boolean)

524
525
526
527
# File 'app/models/project.rb', line 524

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


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

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)

490
491
492
# File 'app/models/project.rb', line 490

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

#projectObject


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

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"

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

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


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

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


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

def rolled_up_trackers
  @rolled_up_trackers ||=
    Tracker.find(:all, :joins => :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


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

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


307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'app/models/project.rb', line 307

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


328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'app/models/project.rb', line 328

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


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

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)


460
461
462
# File 'app/models/project.rb', line 460

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


473
474
475
476
477
478
479
# File 'app/models/project.rb', line 473

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

#to_liquidObject


92
93
94
# File 'app/models/project.rb', line 92

def to_liquid
  ProjectDrop.new(self)
end

#to_paramObject


255
256
257
258
# File 'app/models/project.rb', line 255

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


455
456
457
# File 'app/models/project.rb', line 455

def to_s
  name
end

#unarchiveObject

Unarchives the project All its ancestors must be active


286
287
288
289
# File 'app/models/project.rb', line 286

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.


202
203
204
205
206
207
208
209
# File 'app/models/project.rb', line 202

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


408
409
410
411
412
413
414
415
416
# File 'app/models/project.rb', line 408

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

#visible?Boolean

Is the project visible to the current user

Returns:

  • (Boolean)

145
146
147
# File 'app/models/project.rb', line 145

def visible?
  User.current.allowed_to?(:view_project, self)
end