You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

project.rb 33KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922
  1. # Redmine - project management software
  2. # Copyright (C) 2006-2011 Jean-Philippe Lang
  3. #
  4. # This program is free software; you can redistribute it and/or
  5. # modify it under the terms of the GNU General Public License
  6. # as published by the Free Software Foundation; either version 2
  7. # of the License, or (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  17. class Project < ActiveRecord::Base
  18. include Redmine::SafeAttributes
  19. # Project statuses
  20. STATUS_ACTIVE = 1
  21. STATUS_ARCHIVED = 9
  22. # Maximum length for project identifiers
  23. IDENTIFIER_MAX_LENGTH = 100
  24. # Specific overidden Activities
  25. has_many :time_entry_activities
  26. has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
  27. has_many :memberships, :class_name => 'Member'
  28. has_many :member_principals, :class_name => 'Member',
  29. :include => :principal,
  30. :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
  31. has_many :users, :through => :members
  32. has_many :principals, :through => :member_principals, :source => :principal
  33. has_many :enabled_modules, :dependent => :delete_all
  34. has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
  35. has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
  36. has_many :issue_changes, :through => :issues, :source => :journals
  37. has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
  38. has_many :time_entries, :dependent => :delete_all
  39. has_many :queries, :dependent => :delete_all
  40. has_many :documents, :dependent => :destroy
  41. has_many :news, :dependent => :destroy, :include => :author
  42. has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
  43. has_many :boards, :dependent => :destroy, :order => "position ASC"
  44. has_one :repository, :conditions => ["is_default = ?", true]
  45. has_many :repositories, :dependent => :destroy
  46. has_many :changesets, :through => :repository
  47. has_one :wiki, :dependent => :destroy
  48. # Custom field for the project issues
  49. has_and_belongs_to_many :issue_custom_fields,
  50. :class_name => 'IssueCustomField',
  51. :order => "#{CustomField.table_name}.position",
  52. :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
  53. :association_foreign_key => 'custom_field_id'
  54. acts_as_nested_set :order => 'name', :dependent => :destroy
  55. acts_as_attachable :view_permission => :view_files,
  56. :delete_permission => :manage_files
  57. acts_as_customizable
  58. acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
  59. acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
  60. :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
  61. :author => nil
  62. attr_protected :status
  63. validates_presence_of :name, :identifier
  64. validates_uniqueness_of :identifier
  65. validates_associated :repository, :wiki
  66. validates_length_of :name, :maximum => 255
  67. validates_length_of :homepage, :maximum => 255
  68. validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
  69. # donwcase letters, digits, dashes but not digits only
  70. validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? }
  71. # reserved words
  72. validates_exclusion_of :identifier, :in => %w( new )
  73. before_destroy :delete_all_members
  74. named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
  75. named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
  76. named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
  77. named_scope :all_public, { :conditions => { :is_public => true } }
  78. named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
  79. named_scope :allowed_to, lambda {|*args|
  80. user = User.current
  81. permission = nil
  82. if args.first.is_a?(Symbol)
  83. permission = args.shift
  84. else
  85. user = args.shift
  86. permission = args.shift
  87. end
  88. { :conditions => Project.allowed_to_condition(user, permission, *args) }
  89. }
  90. named_scope :like, lambda {|arg|
  91. if arg.blank?
  92. {}
  93. else
  94. pattern = "%#{arg.to_s.strip.downcase}%"
  95. {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
  96. end
  97. }
  98. def initialize(attributes=nil, *args)
  99. super
  100. initialized = (attributes || {}).stringify_keys
  101. if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
  102. self.identifier = Project.next_identifier
  103. end
  104. if !initialized.key?('is_public')
  105. self.is_public = Setting.default_projects_public?
  106. end
  107. if !initialized.key?('enabled_module_names')
  108. self.enabled_module_names = Setting.default_projects_modules
  109. end
  110. if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
  111. self.trackers = Tracker.all
  112. end
  113. end
  114. def identifier=(identifier)
  115. super unless identifier_frozen?
  116. end
  117. def identifier_frozen?
  118. errors[:identifier].nil? && !(new_record? || identifier.blank?)
  119. end
  120. # returns latest created projects
  121. # non public projects will be returned only if user is a member of those
  122. def self.latest(user=nil, count=5)
  123. visible(user).find(:all, :limit => count, :order => "created_on DESC")
  124. end
  125. # Returns true if the project is visible to +user+ or to the current user.
  126. def visible?(user=User.current)
  127. user.allowed_to?(:view_project, self)
  128. end
  129. # Returns a SQL conditions string used to find all projects visible by the specified user.
  130. #
  131. # Examples:
  132. # Project.visible_condition(admin) => "projects.status = 1"
  133. # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
  134. # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
  135. def self.visible_condition(user, options={})
  136. allowed_to_condition(user, :view_project, options)
  137. end
  138. # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
  139. #
  140. # Valid options:
  141. # * :project => limit the condition to project
  142. # * :with_subprojects => limit the condition to project and its subprojects
  143. # * :member => limit the condition to the user projects
  144. def self.allowed_to_condition(user, permission, options={})
  145. base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
  146. if perm = Redmine::AccessControl.permission(permission)
  147. unless perm.project_module.nil?
  148. # If the permission belongs to a project module, make sure the module is enabled
  149. base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
  150. end
  151. end
  152. if options[:project]
  153. project_statement = "#{Project.table_name}.id = #{options[:project].id}"
  154. project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
  155. base_statement = "(#{project_statement}) AND (#{base_statement})"
  156. end
  157. if user.admin?
  158. base_statement
  159. else
  160. statement_by_role = {}
  161. unless options[:member]
  162. role = user.logged? ? Role.non_member : Role.anonymous
  163. if role.allowed_to?(permission)
  164. statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
  165. end
  166. end
  167. if user.logged?
  168. user.projects_by_role.each do |role, projects|
  169. if role.allowed_to?(permission)
  170. statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
  171. end
  172. end
  173. end
  174. if statement_by_role.empty?
  175. "1=0"
  176. else
  177. if block_given?
  178. statement_by_role.each do |role, statement|
  179. if s = yield(role, user)
  180. statement_by_role[role] = "(#{statement} AND (#{s}))"
  181. end
  182. end
  183. end
  184. "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
  185. end
  186. end
  187. end
  188. # Returns the Systemwide and project specific activities
  189. def activities(include_inactive=false)
  190. if include_inactive
  191. return all_activities
  192. else
  193. return active_activities
  194. end
  195. end
  196. # Will create a new Project specific Activity or update an existing one
  197. #
  198. # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
  199. # does not successfully save.
  200. def update_or_create_time_entry_activity(id, activity_hash)
  201. if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
  202. self.create_time_entry_activity_if_needed(activity_hash)
  203. else
  204. activity = project.time_entry_activities.find_by_id(id.to_i)
  205. activity.update_attributes(activity_hash) if activity
  206. end
  207. end
  208. # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
  209. #
  210. # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
  211. # does not successfully save.
  212. def create_time_entry_activity_if_needed(activity)
  213. if activity['parent_id']
  214. parent_activity = TimeEntryActivity.find(activity['parent_id'])
  215. activity['name'] = parent_activity.name
  216. activity['position'] = parent_activity.position
  217. if Enumeration.overridding_change?(activity, parent_activity)
  218. project_activity = self.time_entry_activities.create(activity)
  219. if project_activity.new_record?
  220. raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
  221. else
  222. self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
  223. end
  224. end
  225. end
  226. end
  227. # Returns a :conditions SQL string that can be used to find the issues associated with this project.
  228. #
  229. # Examples:
  230. # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
  231. # project.project_condition(false) => "projects.id = 1"
  232. def project_condition(with_subprojects)
  233. cond = "#{Project.table_name}.id = #{id}"
  234. cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
  235. cond
  236. end
  237. def self.find(*args)
  238. if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
  239. project = find_by_identifier(*args)
  240. raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
  241. project
  242. else
  243. super
  244. end
  245. end
  246. def self.find_by_param(*args)
  247. self.find(*args)
  248. end
  249. def reload(*args)
  250. @shared_versions = nil
  251. @rolled_up_versions = nil
  252. @rolled_up_trackers = nil
  253. @all_issue_custom_fields = nil
  254. @all_time_entry_custom_fields = nil
  255. @to_param = nil
  256. @allowed_parents = nil
  257. @allowed_permissions = nil
  258. @actions_allowed = nil
  259. super
  260. end
  261. def to_param
  262. # id is used for projects with a numeric identifier (compatibility)
  263. @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
  264. end
  265. def active?
  266. self.status == STATUS_ACTIVE
  267. end
  268. def archived?
  269. self.status == STATUS_ARCHIVED
  270. end
  271. # Archives the project and its descendants
  272. def archive
  273. # Check that there is no issue of a non descendant project that is assigned
  274. # to one of the project or descendant versions
  275. v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
  276. if v_ids.any? && Issue.find(:first, :include => :project,
  277. :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
  278. " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
  279. return false
  280. end
  281. Project.transaction do
  282. archive!
  283. end
  284. true
  285. end
  286. # Unarchives the project
  287. # All its ancestors must be active
  288. def unarchive
  289. return false if ancestors.detect {|a| !a.active?}
  290. update_attribute :status, STATUS_ACTIVE
  291. end
  292. # Returns an array of projects the project can be moved to
  293. # by the current user
  294. def allowed_parents
  295. return @allowed_parents if @allowed_parents
  296. @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
  297. @allowed_parents = @allowed_parents - self_and_descendants
  298. if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
  299. @allowed_parents << nil
  300. end
  301. unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
  302. @allowed_parents << parent
  303. end
  304. @allowed_parents
  305. end
  306. # Sets the parent of the project with authorization check
  307. def set_allowed_parent!(p)
  308. unless p.nil? || p.is_a?(Project)
  309. if p.to_s.blank?
  310. p = nil
  311. else
  312. p = Project.find_by_id(p)
  313. return false unless p
  314. end
  315. end
  316. if p.nil?
  317. if !new_record? && allowed_parents.empty?
  318. return false
  319. end
  320. elsif !allowed_parents.include?(p)
  321. return false
  322. end
  323. set_parent!(p)
  324. end
  325. # Sets the parent of the project
  326. # Argument can be either a Project, a String, a Fixnum or nil
  327. def set_parent!(p)
  328. unless p.nil? || p.is_a?(Project)
  329. if p.to_s.blank?
  330. p = nil
  331. else
  332. p = Project.find_by_id(p)
  333. return false unless p
  334. end
  335. end
  336. if p == parent && !p.nil?
  337. # Nothing to do
  338. true
  339. elsif p.nil? || (p.active? && move_possible?(p))
  340. # Insert the project so that target's children or root projects stay alphabetically sorted
  341. sibs = (p.nil? ? self.class.roots : p.children)
  342. to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
  343. if to_be_inserted_before
  344. move_to_left_of(to_be_inserted_before)
  345. elsif p.nil?
  346. if sibs.empty?
  347. # move_to_root adds the project in first (ie. left) position
  348. move_to_root
  349. else
  350. move_to_right_of(sibs.last) unless self == sibs.last
  351. end
  352. else
  353. # move_to_child_of adds the project in last (ie.right) position
  354. move_to_child_of(p)
  355. end
  356. Issue.update_versions_from_hierarchy_change(self)
  357. true
  358. else
  359. # Can not move to the given target
  360. false
  361. end
  362. end
  363. # Returns an array of the trackers used by the project and its active sub projects
  364. def rolled_up_trackers
  365. @rolled_up_trackers ||=
  366. Tracker.find(:all, :joins => :projects,
  367. :select => "DISTINCT #{Tracker.table_name}.*",
  368. :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
  369. :order => "#{Tracker.table_name}.position")
  370. end
  371. # Closes open and locked project versions that are completed
  372. def close_completed_versions
  373. Version.transaction do
  374. versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
  375. if version.completed?
  376. version.update_attribute(:status, 'closed')
  377. end
  378. end
  379. end
  380. end
  381. # Returns a scope of the Versions on subprojects
  382. def rolled_up_versions
  383. @rolled_up_versions ||=
  384. Version.scoped(:include => :project,
  385. :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
  386. end
  387. # Returns a scope of the Versions used by the project
  388. def shared_versions
  389. if new_record?
  390. Version.scoped(:include => :project,
  391. :conditions => "#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND #{Version.table_name}.sharing = 'system'")
  392. else
  393. @shared_versions ||= begin
  394. r = root? ? self : root
  395. Version.scoped(:include => :project,
  396. :conditions => "#{Project.table_name}.id = #{id}" +
  397. " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
  398. " #{Version.table_name}.sharing = 'system'" +
  399. " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
  400. " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
  401. " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
  402. "))")
  403. end
  404. end
  405. end
  406. # Returns a hash of project users grouped by role
  407. def users_by_role
  408. members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
  409. m.roles.each do |r|
  410. h[r] ||= []
  411. h[r] << m.user
  412. end
  413. h
  414. end
  415. end
  416. # Deletes all project's members
  417. def delete_all_members
  418. me, mr = Member.table_name, MemberRole.table_name
  419. connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
  420. Member.delete_all(['project_id = ?', id])
  421. end
  422. # Users/groups issues can be assigned to
  423. def assignable_users
  424. assignable = Setting.issue_group_assignment? ? member_principals : members
  425. assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
  426. end
  427. # Returns the mail adresses of users that should be always notified on project events
  428. def recipients
  429. notified_users.collect {|user| user.mail}
  430. end
  431. # Returns the users that should be notified on project events
  432. def notified_users
  433. # TODO: User part should be extracted to User#notify_about?
  434. members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
  435. end
  436. # Returns an array of all custom fields enabled for project issues
  437. # (explictly associated custom fields and custom fields enabled for all projects)
  438. def all_issue_custom_fields
  439. @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
  440. end
  441. # Returns an array of all custom fields enabled for project time entries
  442. # (explictly associated custom fields and custom fields enabled for all projects)
  443. def all_time_entry_custom_fields
  444. @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
  445. end
  446. def project
  447. self
  448. end
  449. def <=>(project)
  450. name.downcase <=> project.name.downcase
  451. end
  452. def to_s
  453. name
  454. end
  455. # Returns a short description of the projects (first lines)
  456. def short_description(length = 255)
  457. description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
  458. end
  459. def css_classes
  460. s = 'project'
  461. s << ' root' if root?
  462. s << ' child' if child?
  463. s << (leaf? ? ' leaf' : ' parent')
  464. s
  465. end
  466. # The earliest start date of a project, based on it's issues and versions
  467. def start_date
  468. [
  469. issues.minimum('start_date'),
  470. shared_versions.collect(&:effective_date),
  471. shared_versions.collect(&:start_date)
  472. ].flatten.compact.min
  473. end
  474. # The latest due date of an issue or version
  475. def due_date
  476. [
  477. issues.maximum('due_date'),
  478. shared_versions.collect(&:effective_date),
  479. shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
  480. ].flatten.compact.max
  481. end
  482. def overdue?
  483. active? && !due_date.nil? && (due_date < Date.today)
  484. end
  485. # Returns the percent completed for this project, based on the
  486. # progress on it's versions.
  487. def completed_percent(options={:include_subprojects => false})
  488. if options.delete(:include_subprojects)
  489. total = self_and_descendants.collect(&:completed_percent).sum
  490. total / self_and_descendants.count
  491. else
  492. if versions.count > 0
  493. total = versions.collect(&:completed_pourcent).sum
  494. total / versions.count
  495. else
  496. 100
  497. end
  498. end
  499. end
  500. # Return true if this project is allowed to do the specified action.
  501. # action can be:
  502. # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
  503. # * a permission Symbol (eg. :edit_project)
  504. def allows_to?(action)
  505. if action.is_a? Hash
  506. allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
  507. else
  508. allowed_permissions.include? action
  509. end
  510. end
  511. def module_enabled?(module_name)
  512. module_name = module_name.to_s
  513. enabled_modules.detect {|m| m.name == module_name}
  514. end
  515. def enabled_module_names=(module_names)
  516. if module_names && module_names.is_a?(Array)
  517. module_names = module_names.collect(&:to_s).reject(&:blank?)
  518. self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
  519. else
  520. enabled_modules.clear
  521. end
  522. end
  523. # Returns an array of the enabled modules names
  524. def enabled_module_names
  525. enabled_modules.collect(&:name)
  526. end
  527. # Enable a specific module
  528. #
  529. # Examples:
  530. # project.enable_module!(:issue_tracking)
  531. # project.enable_module!("issue_tracking")
  532. def enable_module!(name)
  533. enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
  534. end
  535. # Disable a module if it exists
  536. #
  537. # Examples:
  538. # project.disable_module!(:issue_tracking)
  539. # project.disable_module!("issue_tracking")
  540. # project.disable_module!(project.enabled_modules.first)
  541. def disable_module!(target)
  542. target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
  543. target.destroy unless target.blank?
  544. end
  545. safe_attributes 'name',
  546. 'description',
  547. 'homepage',
  548. 'is_public',
  549. 'identifier',
  550. 'custom_field_values',
  551. 'custom_fields',
  552. 'tracker_ids',
  553. 'issue_custom_field_ids'
  554. safe_attributes 'enabled_module_names',
  555. :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
  556. # Returns an array of projects that are in this project's hierarchy
  557. #
  558. # Example: parents, children, siblings
  559. def hierarchy
  560. parents = project.self_and_ancestors || []
  561. descendants = project.descendants || []
  562. project_hierarchy = parents | descendants # Set union
  563. end
  564. # Returns an auto-generated project identifier based on the last identifier used
  565. def self.next_identifier
  566. p = Project.find(:first, :order => 'created_on DESC')
  567. p.nil? ? nil : p.identifier.to_s.succ
  568. end
  569. # Copies and saves the Project instance based on the +project+.
  570. # Duplicates the source project's:
  571. # * Wiki
  572. # * Versions
  573. # * Categories
  574. # * Issues
  575. # * Members
  576. # * Queries
  577. #
  578. # Accepts an +options+ argument to specify what to copy
  579. #
  580. # Examples:
  581. # project.copy(1) # => copies everything
  582. # project.copy(1, :only => 'members') # => copies members only
  583. # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
  584. def copy(project, options={})
  585. project = project.is_a?(Project) ? project : Project.find(project)
  586. to_be_copied = %w(wiki versions issue_categories issues members queries boards)
  587. to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
  588. Project.transaction do
  589. if save
  590. reload
  591. to_be_copied.each do |name|
  592. send "copy_#{name}", project
  593. end
  594. Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
  595. save
  596. end
  597. end
  598. end
  599. # Copies +project+ and returns the new instance. This will not save
  600. # the copy
  601. def self.copy_from(project)
  602. begin
  603. project = project.is_a?(Project) ? project : Project.find(project)
  604. if project
  605. # clear unique attributes
  606. attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
  607. copy = Project.new(attributes)
  608. copy.enabled_modules = project.enabled_modules
  609. copy.trackers = project.trackers
  610. copy.custom_values = project.custom_values.collect {|v| v.clone}
  611. copy.issue_custom_fields = project.issue_custom_fields
  612. return copy
  613. else
  614. return nil
  615. end
  616. rescue ActiveRecord::RecordNotFound
  617. return nil
  618. end
  619. end
  620. # Yields the given block for each project with its level in the tree
  621. def self.project_tree(projects, &block)
  622. ancestors = []
  623. projects.sort_by(&:lft).each do |project|
  624. while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
  625. ancestors.pop
  626. end
  627. yield project, ancestors.size
  628. ancestors << project
  629. end
  630. end
  631. private
  632. # Copies wiki from +project+
  633. def copy_wiki(project)
  634. # Check that the source project has a wiki first
  635. unless project.wiki.nil?
  636. self.wiki ||= Wiki.new
  637. wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
  638. wiki_pages_map = {}
  639. project.wiki.pages.each do |page|
  640. # Skip pages without content
  641. next if page.content.nil?
  642. new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
  643. new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
  644. new_wiki_page.content = new_wiki_content
  645. wiki.pages << new_wiki_page
  646. wiki_pages_map[page.id] = new_wiki_page
  647. end
  648. wiki.save
  649. # Reproduce page hierarchy
  650. project.wiki.pages.each do |page|
  651. if page.parent_id && wiki_pages_map[page.id]
  652. wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
  653. wiki_pages_map[page.id].save
  654. end
  655. end
  656. end
  657. end
  658. # Copies versions from +project+
  659. def copy_versions(project)
  660. project.versions.each do |version|
  661. new_version = Version.new
  662. new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
  663. self.versions << new_version
  664. end
  665. end
  666. # Copies issue categories from +project+
  667. def copy_issue_categories(project)
  668. project.issue_categories.each do |issue_category|
  669. new_issue_category = IssueCategory.new
  670. new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
  671. self.issue_categories << new_issue_category
  672. end
  673. end
  674. # Copies issues from +project+
  675. # Note: issues assigned to a closed version won't be copied due to validation rules
  676. def copy_issues(project)
  677. # Stores the source issue id as a key and the copied issues as the
  678. # value. Used to map the two togeather for issue relations.
  679. issues_map = {}
  680. # Get issues sorted by root_id, lft so that parent issues
  681. # get copied before their children
  682. project.issues.find(:all, :order => 'root_id, lft').each do |issue|
  683. new_issue = Issue.new
  684. new_issue.copy_from(issue)
  685. new_issue.project = self
  686. # Reassign fixed_versions by name, since names are unique per
  687. # project and the versions for self are not yet saved
  688. if issue.fixed_version
  689. new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
  690. end
  691. # Reassign the category by name, since names are unique per
  692. # project and the categories for self are not yet saved
  693. if issue.category
  694. new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
  695. end
  696. # Parent issue
  697. if issue.parent_id
  698. if copied_parent = issues_map[issue.parent_id]
  699. new_issue.parent_issue_id = copied_parent.id
  700. end
  701. end
  702. self.issues << new_issue
  703. if new_issue.new_record?
  704. logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
  705. else
  706. issues_map[issue.id] = new_issue unless new_issue.new_record?
  707. end
  708. end
  709. # Relations after in case issues related each other
  710. project.issues.each do |issue|
  711. new_issue = issues_map[issue.id]
  712. unless new_issue
  713. # Issue was not copied
  714. next
  715. end
  716. # Relations
  717. issue.relations_from.each do |source_relation|
  718. new_issue_relation = IssueRelation.new
  719. new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
  720. new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
  721. if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
  722. new_issue_relation.issue_to = source_relation.issue_to
  723. end
  724. new_issue.relations_from << new_issue_relation
  725. end
  726. issue.relations_to.each do |source_relation|
  727. new_issue_relation = IssueRelation.new
  728. new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
  729. new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
  730. if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
  731. new_issue_relation.issue_from = source_relation.issue_from
  732. end
  733. new_issue.relations_to << new_issue_relation
  734. end
  735. end
  736. end
  737. # Copies members from +project+
  738. def copy_members(project)
  739. # Copy users first, then groups to handle members with inherited and given roles
  740. members_to_copy = []
  741. members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
  742. members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
  743. members_to_copy.each do |member|
  744. new_member = Member.new
  745. new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
  746. # only copy non inherited roles
  747. # inherited roles will be added when copying the group membership
  748. role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
  749. next if role_ids.empty?
  750. new_member.role_ids = role_ids
  751. new_member.project = self
  752. self.members << new_member
  753. end
  754. end
  755. # Copies queries from +project+
  756. def copy_queries(project)
  757. project.queries.each do |query|
  758. new_query = ::Query.new
  759. new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
  760. new_query.sort_criteria = query.sort_criteria if query.sort_criteria
  761. new_query.project = self
  762. new_query.user_id = query.user_id
  763. self.queries << new_query
  764. end
  765. end
  766. # Copies boards from +project+
  767. def copy_boards(project)
  768. project.boards.each do |board|
  769. new_board = Board.new
  770. new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
  771. new_board.project = self
  772. self.boards << new_board
  773. end
  774. end
  775. def allowed_permissions
  776. @allowed_permissions ||= begin
  777. module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
  778. Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
  779. end
  780. end
  781. def allowed_actions
  782. @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
  783. end
  784. # Returns all the active Systemwide and project specific activities
  785. def active_activities
  786. overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
  787. if overridden_activity_ids.empty?
  788. return TimeEntryActivity.shared.active
  789. else
  790. return system_activities_and_project_overrides
  791. end
  792. end
  793. # Returns all the Systemwide and project specific activities
  794. # (inactive and active)
  795. def all_activities
  796. overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
  797. if overridden_activity_ids.empty?
  798. return TimeEntryActivity.shared
  799. else
  800. return system_activities_and_project_overrides(true)
  801. end
  802. end
  803. # Returns the systemwide active activities merged with the project specific overrides
  804. def system_activities_and_project_overrides(include_inactive=false)
  805. if include_inactive
  806. return TimeEntryActivity.shared.
  807. find(:all,
  808. :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
  809. self.time_entry_activities
  810. else
  811. return TimeEntryActivity.shared.active.
  812. find(:all,
  813. :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
  814. self.time_entry_activities.active
  815. end
  816. end
  817. # Archives subprojects recursively
  818. def archive!
  819. children.each do |subproject|
  820. subproject.send :archive!
  821. end
  822. update_attribute :status, STATUS_ARCHIVED
  823. end
  824. end