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-2014 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 collapse

STATUS_ACTIVE =

Project statuses

1
STATUS_CLOSED =
5
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?, #safe_attribute_names, #safe_attributes=

Constructor Details

#initialize(attributes = nil, *args) ⇒ Project

Returns a new instance of Project



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'app/models/project.rb', line 116

def initialize(attributes=nil, *args)
  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')
    default = Setting.default_projects_tracker_ids
    if default.is_a?(Array)
      self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
    else
      self.trackers = Tracker.sorted.to_a
    end
  end
end

Class Method Details

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

Returns a SQL conditions string used to find all projects for which user has the given permission

Valid options:

  • :project => limit the condition to project

  • :with_subprojects => limit the condition to project and its subprojects

  • :member => limit the condition to the user projects



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'app/models/project.rb', line 174

def self.allowed_to_condition(user, permission, options={})
  perm = Redmine::AccessControl.permission(permission)
  base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
  if perm && perm.project_module
    # 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
  if project = options[:project]
    project_statement = project.project_condition(options[:with_subprojects])
    base_statement = "(#{project_statement}) AND (#{base_statement})"
  end

  if user.admin?
    base_statement
  else
    statement_by_role = {}
    unless options[:member]
      role = user.builtin_role
      if role.allowed_to?(permission)
        statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
      end
    end
    user.projects_by_role.each do |role, projects|
      if role.allowed_to?(permission) && projects.any?
        statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
      end
    end
    if statement_by_role.empty?
      "1=0"
    else
      if block_given?
        statement_by_role.each do |role, statement|
          if s = yield(role, user)
            statement_by_role[role] = "(#{statement} AND (#{s}))"
          end
        end
      end
      "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
    end
  end
end

.copy_from(project) ⇒ Object

Returns a new unsaved Project instance with attributes copied from project



758
759
760
761
762
763
764
765
766
767
768
# File 'app/models/project.rb', line 758

def self.copy_from(project)
  project = project.is_a?(Project) ? project : Project.find(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
  copy
end

.find(*args) ⇒ Object



285
286
287
288
289
290
291
292
293
# File 'app/models/project.rb', line 285

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

.find_by_param(*args) ⇒ Object



295
296
297
# File 'app/models/project.rb', line 295

def self.find_by_param(*args)
  self.find(*args)
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



149
150
151
# File 'app/models/project.rb', line 149

def self.latest(user=nil, count=5)
  visible(user).limit(count).order("created_on DESC").to_a
end

.next_identifierObject

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



718
719
720
721
# File 'app/models/project.rb', line 718

def self.next_identifier
  p = Project.order('id DESC').first
  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



771
772
773
774
775
776
777
778
779
780
# File 'app/models/project.rb', line 771

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

.rebuild_tree!Object

Recalculates all lft and rgt values based on project names Unlike Project.rebuild!, these values are recalculated even if the tree “looks” valid Used in BuildProjectsTree migration



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

def self.rebuild_tree!
  transaction do
    update_all "lft = NULL, rgt = NULL"
    rebuild!(false)
    all.each { |p| p.set_or_update_position_under(p.parent) }
  end
end

.visible_condition(user, options = {}) ⇒ Object

Returns a SQL conditions string used to find all projects visible by the specified user.

Examples:

Project.visible_condition(admin)        => "projects.status = 1"
Project.visible_condition(normal_user)  => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
Project.visible_condition(anonymous)    => "((projects.status = 1) AND (projects.is_public = 1))"


164
165
166
# File 'app/models/project.rb', line 164

def self.visible_condition(user, options={})
  allowed_to_condition(user, :view_project, options)
end

Instance Method Details

#<=>(project) ⇒ Object



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

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

#active?Boolean

Returns:

  • (Boolean)


324
325
326
# File 'app/models/project.rb', line 324

def active?
  self.status == STATUS_ACTIVE
end

#activities(include_inactive = false) ⇒ Object

Returns the Systemwide and project specific activities



231
232
233
234
235
236
237
# File 'app/models/project.rb', line 231

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

#add_default_member(user) ⇒ Object

Adds user as a project member with the default role Used for when a non-admin user creates a project



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

def add_default_member(user)
  role = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
  member = Member.new(:project => self, :principal => user, :roles => [role])
  self.members << member
  member
end

#all_issue_custom_fieldsObject

Returns a scope of all custom fields enabled for project issues (explicitly associated custom fields and custom fields enabled for all projects)



545
546
547
548
549
550
551
# File 'app/models/project.rb', line 545

def all_issue_custom_fields
  @all_issue_custom_fields ||= IssueCustomField.
    sorted.
    where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
      " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
      " WHERE cfp.project_id = ?)", true, id)
end

#allowed_parentsObject

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



369
370
371
372
373
374
375
376
377
378
379
380
# File 'app/models/project.rb', line 369

def allowed_parents
  return @allowed_parents if @allowed_parents
  @allowed_parents = Project.allowed_to(User.current, :add_subprojects).to_a
  @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 allows 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)


629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
# File 'app/models/project.rb', line 629

def allows_to?(action)
  if archived?
    # No action allowed on archived projects
    return false
  end
  unless active? || Redmine::AccessControl.read_action?(action)
    # No write action allowed on closed projects
    return false
  end
  # No action allowed on disabled modules
  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



333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'app/models/project.rb', line 333

def archive
  # Check that there is no issue of a non descendant project that is assigned
  # to one of the project or descendant versions
  version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")

  if version_ids.any? &&
    Issue.
      includes(:project).
      where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
      where(:fixed_version_id => version_ids).
      exists?
    return false
  end
  Project.transaction do
    archive!
  end
  true
end

#archived?Boolean

Returns:

  • (Boolean)


328
329
330
# File 'app/models/project.rb', line 328

def archived?
  self.status == STATUS_ARCHIVED
end

#assignable_usersObject

Return a Principal scope of users/groups issues can be assigned to



520
521
522
523
524
525
526
527
528
529
530
# File 'app/models/project.rb', line 520

def assignable_users
  types = ['User']
  types << 'Group' if Setting.issue_group_assignment?

  @assignable_users ||= Principal.
    active.
    joins(:members => :roles).
    where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
    uniq.
    sorted
end

#base_reloadObject



299
# File 'app/models/project.rb', line 299

alias :base_reload :reload

#closeObject



359
360
361
# File 'app/models/project.rb', line 359

def close
  self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
end

#close_completed_versionsObject

Closes open and locked project versions that are completed



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

def close_completed_versions
  Version.transaction do
    versions.where(: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.



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

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_percent).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


738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
# File 'app/models/project.rb', line 738

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 & Array.wrap(options[:only]) 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
  true
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.



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

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.overriding_change?(activity, parent_activity)
      project_activity = self.time_entry_activities.create(activity)
      if project_activity.new_record?
        raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
      else
        self.time_entries.
          where(["activity_id = ?", parent_activity.id]).
          update_all("activity_id = #{project_activity.id}")
      end
    end
  end
end

#css_classesObject



570
571
572
573
574
575
576
577
578
579
580
581
582
583
# File 'app/models/project.rb', line 570

def css_classes
  s = 'project'
  s << ' root' if root?
  s << ' child' if child?
  s << (leaf? ? ' leaf' : ' parent')
  unless active?
    if archived?
      s << ' archived'
    else
      s << ' closed'
    end
  end
  s
end

#delete_all_membersObject

Deletes all project's members



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

def delete_all_members
  me, mr = Member.table_name, MemberRole.table_name
  self.class.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

#disable_module!(target) ⇒ Object

Disable a module if it exists

Examples:

project.disable_module!(:issue_tracking)
project.disable_module!("issue_tracking")
project.disable_module!(project.enabled_modules.first)


687
688
689
690
# File 'app/models/project.rb', line 687

def disable_module!(target)
  target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
  target.destroy unless target.blank?
end

#due_dateObject

The latest due date of an issue or version



595
596
597
598
599
600
601
# File 'app/models/project.rb', line 595

def due_date
  @due_date ||= [
   issues.maximum('due_date'),
   shared_versions.maximum('effective_date'),
   Issue.fixed_version(shared_versions).maximum('due_date')
  ].compact.max
end

#enable_module!(name) ⇒ Object

Enable a specific module

Examples:

project.enable_module!(:issue_tracking)
project.enable_module!("issue_tracking")


677
678
679
# File 'app/models/project.rb', line 677

def enable_module!(name)
  enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
end

#enabled_module(name) ⇒ Object

Return the enabled module with the given name or nil if the module is not enabled for the project



648
649
650
651
# File 'app/models/project.rb', line 648

def enabled_module(name)
  name = name.to_s
  enabled_modules.detect {|m| m.name == name}
end

#enabled_module_namesObject

Returns an array of the enabled modules names



668
669
670
# File 'app/models/project.rb', line 668

def enabled_module_names
  enabled_modules.collect(&:name)
end

#enabled_module_names=(module_names) ⇒ Object



658
659
660
661
662
663
664
665
# File 'app/models/project.rb', line 658

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



711
712
713
714
715
# File 'app/models/project.rb', line 711

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

#identifier=(identifier) ⇒ Object



139
140
141
# File 'app/models/project.rb', line 139

def identifier=(identifier)
  super unless identifier_frozen?
end

#identifier_frozen?Boolean

Returns:

  • (Boolean)


143
144
145
# File 'app/models/project.rb', line 143

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

#module_enabled?(name) ⇒ Boolean

Return true if the module with the given name is enabled

Returns:

  • (Boolean)


654
655
656
# File 'app/models/project.rb', line 654

def module_enabled?(name)
  enabled_module(name).present?
end

#notified_usersObject

Returns the users that should be notified on project events



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

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

#overdue?Boolean

Returns:

  • (Boolean)


603
604
605
# File 'app/models/project.rb', line 603

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

#override_roles(role) ⇒ Object



216
217
218
219
220
# File 'app/models/project.rb', line 216

def override_roles(role)
  group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
  member = member_principals.where("#{Principal.table_name}.type = ?", group_class.name).first
  member ? member.roles.to_a : [role]
end

#principalsObject



222
223
224
# File 'app/models/project.rb', line 222

def principals
  @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
end

#projectObject



553
554
555
# File 'app/models/project.rb', line 553

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"


279
280
281
282
283
# File 'app/models/project.rb', line 279

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 addresses of users that should be always notified on project events



533
534
535
# File 'app/models/project.rb', line 533

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

#reload(*args) ⇒ Object



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'app/models/project.rb', line 300

def reload(*args)
  @principals = nil
  @users = nil
  @shared_versions = nil
  @rolled_up_versions = nil
  @rolled_up_trackers = nil
  @all_issue_custom_fields = nil
  @all_time_entry_custom_fields = nil
  @to_param = nil
  @allowed_parents = nil
  @allowed_permissions = nil
  @actions_allowed = nil
  @start_date = nil
  @due_date = nil
  @override_members = nil
  @assignable_users = nil
  base_reload(*args)
end

#reopenObject



363
364
365
# File 'app/models/project.rb', line 363

def reopen
  self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
end

#rolled_up_trackersObject

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



438
439
440
441
442
443
444
445
446
447
# File 'app/models/project.rb', line 438

def rolled_up_trackers
  @rolled_up_trackers ||=
    Tracker.
      joins(:projects).
      joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
      where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED).
      uniq.
      sorted.
      to_a
end

#rolled_up_versionsObject

Returns a scope of the Versions on subprojects



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

def rolled_up_versions
  @rolled_up_versions ||=
    Version.
      joins(:project).
      where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
end

#set_allowed_parent!(p) ⇒ Object

Sets the parent of the project with authorization check



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'app/models/project.rb', line 383

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_or_update_position_under(target_parent) ⇒ Object

Inserts/moves the project so that target's children or root projects stay alphabetically sorted



1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
# File 'app/models/project.rb', line 1054

def set_or_update_position_under(target_parent)
  parent_was = parent
  sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
  to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.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 target_parent.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(target_parent)
  end
  if parent_was != target_parent
    after_parent_changed(parent_was)
  end
end

#set_parent!(p) ⇒ Object

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



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# File 'app/models/project.rb', line 404

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))
    set_or_update_position_under(p)
    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



469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
# File 'app/models/project.rb', line 469

def shared_versions
  if new_record?
    Version.
      joins(:project).
      preload(:project).
      where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
  else
    @shared_versions ||= begin
      r = root? ? self : root
      Version.
        joins(:project).
        preload(:project).
        where("#{Project.table_name}.id = #{id}" +
                " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} 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
end

#short_description(length = 255) ⇒ Object

Returns a short description of the projects (first lines)



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

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



586
587
588
589
590
591
592
# File 'app/models/project.rb', line 586

def start_date
  @start_date ||= [
   issues.minimum('start_date'),
   shared_versions.minimum('effective_date'),
   Issue.fixed_version(shared_versions).minimum('start_date')
  ].compact.min
end

#to_paramObject



319
320
321
322
# File 'app/models/project.rb', line 319

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

#to_sObject



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

def to_s
  name
end

#unarchiveObject

Unarchives the project All its ancestors must be active



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

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.



243
244
245
246
247
248
249
250
# File 'app/models/project.rb', line 243

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

#usersObject



226
227
228
# File 'app/models/project.rb', line 226

def users
  @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
end

#users_by_roleObject

Returns a hash of project users grouped by role



493
494
495
496
497
498
499
500
501
# File 'app/models/project.rb', line 493

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

#visible?(user = User.current) ⇒ Boolean

Returns true if the project is visible to user or to the current user.

Returns:

  • (Boolean)


154
155
156
# File 'app/models/project.rb', line 154

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