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.

issue.rb 68KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101
  1. # frozen_string_literal: true
  2. # Redmine - project management software
  3. # Copyright (C) 2006-2023 Jean-Philippe Lang
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU General Public License
  7. # as published by the Free Software Foundation; either version 2
  8. # of the License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. class Issue < ApplicationRecord
  19. include Redmine::SafeAttributes
  20. include Redmine::Utils::DateCalculation
  21. include Redmine::I18n
  22. before_save :set_parent_id
  23. include Redmine::NestedSet::IssueNestedSet
  24. belongs_to :project
  25. belongs_to :tracker
  26. belongs_to :status, :class_name => 'IssueStatus'
  27. belongs_to :author, :class_name => 'User'
  28. belongs_to :assigned_to, :class_name => 'Principal'
  29. belongs_to :fixed_version, :class_name => 'Version'
  30. belongs_to :priority, :class_name => 'IssuePriority'
  31. belongs_to :category, :class_name => 'IssueCategory'
  32. has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
  33. has_many :time_entries, :dependent => :destroy
  34. has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
  35. has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
  36. has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
  37. acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
  38. acts_as_customizable
  39. acts_as_watchable
  40. acts_as_searchable :columns => ['subject', "#{table_name}.description"],
  41. :preload => [:project, :status, :tracker],
  42. :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
  43. acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
  44. :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
  45. :type => Proc.new {|o| 'issue' + (o.closed? ? '-closed' : '')}
  46. acts_as_activity_provider :scope => proc {preload(:project, :author, :tracker, :status)},
  47. :author_key => :author_id
  48. acts_as_mentionable :attributes => ['description']
  49. DONE_RATIO_OPTIONS = %w(issue_field issue_status)
  50. attr_reader :transition_warning
  51. attr_writer :deleted_attachment_ids
  52. delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
  53. validates_presence_of :subject, :project, :tracker
  54. validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
  55. validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
  56. validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
  57. validates_length_of :subject, :maximum => 255
  58. validates_inclusion_of :done_ratio, :in => 0..100
  59. validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
  60. validates :start_date, :date => true
  61. validates :due_date, :date => true
  62. validate :validate_issue, :validate_required_fields, :validate_permissions
  63. scope :visible, (lambda do |*args|
  64. joins(:project).
  65. where(Issue.visible_condition(args.shift || User.current, *args))
  66. end)
  67. scope :open, (lambda do |*args|
  68. is_closed = !args.empty? ? !args.first : false
  69. joins(:status).
  70. where(:issue_statuses => {:is_closed => is_closed})
  71. end)
  72. scope :recently_updated, lambda {order(:updated_on => :desc)}
  73. scope :on_active_project, (lambda do
  74. joins(:project).
  75. where(:projects => {:status => Project::STATUS_ACTIVE})
  76. end)
  77. scope :fixed_version, (lambda do |versions|
  78. ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
  79. ids.any? ? where(:fixed_version_id => ids) : none
  80. end)
  81. scope :assigned_to, (lambda do |arg|
  82. arg = Array(arg).uniq
  83. ids = arg.map {|p| p.is_a?(Principal) ? p.id : p}
  84. ids += arg.select {|p| p.is_a?(User)}.map(&:group_ids).flatten.uniq
  85. ids.compact!
  86. ids.any? ? where(:assigned_to_id => ids) : none
  87. end)
  88. scope :like, (lambda do |q|
  89. if q.present?
  90. where(*::Query.tokenized_like_conditions("#{table_name}.subject", q))
  91. end
  92. end)
  93. before_validation :default_assign, on: :create
  94. before_validation :clear_disabled_fields
  95. before_save :close_duplicates, :update_done_ratio_from_issue_status,
  96. :force_updated_on_change, :update_closed_on
  97. after_save do |issue|
  98. if !issue.saved_change_to_id? && issue.saved_change_to_project_id?
  99. issue.send :after_project_change
  100. end
  101. end
  102. after_save :reschedule_following_issues, :update_nested_set_attributes,
  103. :update_parent_attributes, :delete_selected_attachments, :create_journal
  104. # Should be after_create but would be called before previous after_save callbacks
  105. after_save :after_create_from_copy
  106. after_destroy :update_parent_attributes
  107. # add_auto_watcher needs to run before sending notifications, thus it needs
  108. # to be added after send_notification (after_ callbacks are run in inverse order)
  109. # https://api.rubyonrails.org/v5.2.3/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback
  110. after_create_commit :send_notification
  111. after_create_commit :add_auto_watcher
  112. after_commit :create_parent_issue_journal
  113. # Returns a SQL conditions string used to find all issues visible by the specified user
  114. def self.visible_condition(user, options={})
  115. Project.allowed_to_condition(user, :view_issues, options) do |role, user|
  116. sql =
  117. if user.id && user.logged?
  118. case role.issues_visibility
  119. when 'all'
  120. '1=1'
  121. when 'default'
  122. user_ids = [user.id] + user.groups.pluck(:id).compact
  123. "(#{table_name}.is_private = #{connection.quoted_false} " \
  124. "OR #{table_name}.author_id = #{user.id} " \
  125. "OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
  126. when 'own'
  127. user_ids = [user.id] + user.groups.pluck(:id).compact
  128. "(#{table_name}.author_id = #{user.id} OR " \
  129. "#{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
  130. else
  131. '1=0'
  132. end
  133. else
  134. "(#{table_name}.is_private = #{connection.quoted_false})"
  135. end
  136. unless role.permissions_all_trackers?(:view_issues)
  137. tracker_ids = role.permissions_tracker_ids(:view_issues)
  138. if tracker_ids.any?
  139. sql = "(#{sql} AND #{table_name}.tracker_id IN (#{tracker_ids.join(',')}))"
  140. else
  141. sql = '1=0'
  142. end
  143. end
  144. sql
  145. end
  146. end
  147. # Returns true if usr or current user is allowed to view the issue
  148. def visible?(usr=nil)
  149. (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
  150. visible =
  151. if user.logged?
  152. case role.issues_visibility
  153. when 'all'
  154. true
  155. when 'default'
  156. !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
  157. when 'own'
  158. self.author == user || user.is_or_belongs_to?(assigned_to)
  159. else
  160. false
  161. end
  162. else
  163. !self.is_private?
  164. end
  165. unless role.permissions_all_trackers?(:view_issues)
  166. visible &&= role.permissions_tracker_ids?(:view_issues, tracker_id)
  167. end
  168. visible
  169. end
  170. end
  171. # Returns true if user or current user is allowed to edit or add notes to the issue
  172. def editable?(user=User.current)
  173. attributes_editable?(user) || notes_addable?(user)
  174. end
  175. # Returns true if user or current user is allowed to edit the issue
  176. def attributes_editable?(user=User.current)
  177. user_tracker_permission?(user, :edit_issues) || (
  178. user_tracker_permission?(user, :edit_own_issues) && author == user
  179. )
  180. end
  181. def attachments_addable?(user=User.current)
  182. attributes_editable?(user) || notes_addable?(user)
  183. end
  184. # Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_editable?
  185. def attachments_editable?(user=User.current)
  186. attributes_editable?(user)
  187. end
  188. # Returns true if user or current user is allowed to add notes to the issue
  189. def notes_addable?(user=User.current)
  190. user_tracker_permission?(user, :add_issue_notes)
  191. end
  192. # Returns true if user or current user is allowed to delete the issue
  193. def deletable?(user=User.current)
  194. user_tracker_permission?(user, :delete_issues)
  195. end
  196. # Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_deletable?
  197. def attachments_deletable?(user=User.current)
  198. attributes_editable?(user)
  199. end
  200. def initialize(attributes=nil, *args)
  201. super
  202. if new_record?
  203. # set default values for new records only
  204. self.priority ||= IssuePriority.default
  205. self.watcher_user_ids = []
  206. end
  207. end
  208. def create_or_update(*args)
  209. super()
  210. ensure
  211. @status_was = nil
  212. end
  213. private :create_or_update
  214. # AR#Persistence#destroy would raise and RecordNotFound exception
  215. # if the issue was already deleted or updated (non matching lock_version).
  216. # This is a problem when bulk deleting issues or deleting a project
  217. # (because an issue may already be deleted if its parent was deleted
  218. # first).
  219. # The issue is reloaded by the nested_set before being deleted so
  220. # the lock_version condition should not be an issue but we handle it.
  221. def destroy
  222. super
  223. rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
  224. # Stale or already deleted
  225. begin
  226. reload
  227. rescue ActiveRecord::RecordNotFound
  228. # The issue was actually already deleted
  229. @destroyed = true
  230. return freeze
  231. end
  232. # The issue was stale, retry to destroy
  233. super
  234. end
  235. alias :base_reload :reload
  236. def reload(*args)
  237. @workflow_rule_by_attribute = nil
  238. @assignable_versions = nil
  239. @relations = nil
  240. @spent_hours = nil
  241. @total_spent_hours = nil
  242. @total_estimated_hours = nil
  243. @last_updated_by = nil
  244. @last_notes = nil
  245. base_reload(*args)
  246. end
  247. # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
  248. def available_custom_fields
  249. (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
  250. end
  251. def visible_custom_field_values(user=nil)
  252. user_real = user || User.current
  253. custom_field_values.select do |value|
  254. value.custom_field.visible_by?(project, user_real)
  255. end
  256. end
  257. # Overrides Redmine::Acts::Customizable::InstanceMethods#set_custom_field_default?
  258. def set_custom_field_default?(custom_value)
  259. new_record? || project_id_changed?|| tracker_id_changed?
  260. end
  261. # Copies attributes from another issue, arg can be an id or an Issue
  262. def copy_from(arg, options={})
  263. issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
  264. self.attributes =
  265. issue.attributes.dup.except(
  266. "id", "root_id", "parent_id", "lft", "rgt",
  267. "created_on", "updated_on", "status_id", "closed_on"
  268. )
  269. self.custom_field_values =
  270. issue.custom_field_values.inject({}) do |h, v|
  271. h[v.custom_field_id] = v.value
  272. h
  273. end
  274. if options[:keep_status]
  275. self.status = issue.status
  276. end
  277. self.author = User.current
  278. unless options[:attachments] == false
  279. self.attachments = issue.attachments.map do |attachement|
  280. attachement.copy(:container => self)
  281. end
  282. end
  283. unless options[:watchers] == false
  284. self.watcher_user_ids =
  285. issue.watcher_users.select{|u| u.status == User::STATUS_ACTIVE}.map(&:id)
  286. end
  287. @copied_from = issue
  288. @copy_options = options
  289. self
  290. end
  291. # Returns an unsaved copy of the issue
  292. def copy(attributes=nil, copy_options={})
  293. copy = self.class.new.copy_from(self, copy_options)
  294. copy.attributes = attributes if attributes
  295. copy
  296. end
  297. # Returns true if the issue is a copy
  298. def copy?
  299. @copied_from.present?
  300. end
  301. def status_id=(status_id)
  302. if status_id.to_s != self.status_id.to_s
  303. self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
  304. end
  305. self.status_id
  306. end
  307. # Sets the status.
  308. def status=(status)
  309. if status != self.status
  310. @workflow_rule_by_attribute = nil
  311. end
  312. association(:status).writer(status)
  313. end
  314. def priority_id=(pid)
  315. self.priority = nil
  316. write_attribute(:priority_id, pid)
  317. end
  318. def category_id=(cid)
  319. self.category = nil
  320. write_attribute(:category_id, cid)
  321. end
  322. def fixed_version_id=(vid)
  323. self.fixed_version = nil
  324. write_attribute(:fixed_version_id, vid)
  325. end
  326. def tracker_id=(tracker_id)
  327. if tracker_id.to_s != self.tracker_id.to_s
  328. self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
  329. end
  330. self.tracker_id
  331. end
  332. # Sets the tracker.
  333. # This will set the status to the default status of the new tracker if:
  334. # * the status was the default for the previous tracker
  335. # * or if the status was not part of the new tracker statuses
  336. # * or the status was nil
  337. def tracker=(tracker)
  338. tracker_was = self.tracker
  339. association(:tracker).writer(tracker)
  340. if tracker != tracker_was
  341. if status == tracker_was.try(:default_status)
  342. self.status = nil
  343. elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
  344. self.status = nil
  345. end
  346. reassign_custom_field_values
  347. @workflow_rule_by_attribute = nil
  348. end
  349. self.status ||= default_status
  350. self.tracker
  351. end
  352. def project_id=(project_id)
  353. if project_id.to_s != self.project_id.to_s
  354. self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
  355. end
  356. self.project_id
  357. end
  358. # Sets the project.
  359. # Unless keep_tracker argument is set to true, this will change the tracker
  360. # to the first tracker of the new project if the previous tracker is not part
  361. # of the new project trackers.
  362. # This will:
  363. # * clear the fixed_version is it's no longer valid for the new project.
  364. # * clear the parent issue if it's no longer valid for the new project.
  365. # * set the category to the category with the same name in the new
  366. # project if it exists, or clear it if it doesn't.
  367. # * for new issue, set the fixed_version to the project default version
  368. # if it's a valid fixed_version.
  369. def project=(project, keep_tracker=false)
  370. project_was = self.project
  371. association(:project).writer(project)
  372. if project != project_was
  373. @safe_attribute_names = nil
  374. end
  375. if project_was && project && project_was != project
  376. @assignable_versions = nil
  377. unless keep_tracker || project.trackers.include?(tracker)
  378. self.tracker = project.trackers.first
  379. end
  380. # Reassign to the category with same name if any
  381. if category
  382. self.category = project.issue_categories.find_by_name(category.name)
  383. end
  384. # Clear the assignee if not available in the new project for new issues (eg. copy)
  385. # For existing issue, the previous assignee is still valid, so we keep it
  386. if new_record? && assigned_to && !assignable_users.include?(assigned_to)
  387. self.assigned_to_id = nil
  388. end
  389. # Keep the fixed_version if it's still valid in the new_project
  390. if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
  391. self.fixed_version = nil
  392. end
  393. # Clear the parent task if it's no longer valid
  394. unless valid_parent_project?
  395. self.parent_issue_id = nil
  396. end
  397. reassign_custom_field_values
  398. @workflow_rule_by_attribute = nil
  399. end
  400. # Set fixed_version to the project default version if it's valid
  401. if new_record? && fixed_version.nil? && project && project.default_version_id?
  402. if project.shared_versions.open.exists?(project.default_version_id)
  403. self.fixed_version_id = project.default_version_id
  404. end
  405. end
  406. self.project
  407. end
  408. def description=(arg)
  409. if arg.is_a?(String)
  410. arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
  411. end
  412. write_attribute(:description, arg)
  413. end
  414. def deleted_attachment_ids
  415. Array(@deleted_attachment_ids).map(&:to_i)
  416. end
  417. # Overrides assign_attributes so that project and tracker get assigned first
  418. def assign_attributes(new_attributes, *args)
  419. return if new_attributes.nil?
  420. attrs = new_attributes.dup
  421. attrs.stringify_keys!
  422. %w(project project_id tracker tracker_id).each do |attr|
  423. if attrs.has_key?(attr)
  424. send :"#{attr}=", attrs.delete(attr)
  425. end
  426. end
  427. super(attrs, *args)
  428. end
  429. def attributes=(new_attributes)
  430. assign_attributes new_attributes
  431. end
  432. def estimated_hours=(h)
  433. write_attribute :estimated_hours, (h.is_a?(String) ? (h.to_hours || h) : h)
  434. end
  435. safe_attributes(
  436. 'project_id',
  437. 'tracker_id',
  438. 'status_id',
  439. 'category_id',
  440. 'assigned_to_id',
  441. 'priority_id',
  442. 'fixed_version_id',
  443. 'subject',
  444. 'description',
  445. 'start_date',
  446. 'due_date',
  447. 'done_ratio',
  448. 'estimated_hours',
  449. 'custom_field_values',
  450. 'custom_fields',
  451. 'lock_version',
  452. :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user)})
  453. safe_attributes(
  454. 'notes',
  455. :if => lambda {|issue, user| issue.notes_addable?(user)})
  456. safe_attributes(
  457. 'private_notes',
  458. :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)})
  459. safe_attributes(
  460. 'watcher_user_ids',
  461. :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)})
  462. safe_attributes(
  463. 'is_private',
  464. :if => lambda do |issue, user|
  465. user.allowed_to?(:set_issues_private, issue.project) ||
  466. (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
  467. end)
  468. safe_attributes(
  469. 'parent_issue_id',
  470. :if => lambda do |issue, user|
  471. (issue.new_record? || issue.attributes_editable?(user)) &&
  472. user.allowed_to?(:manage_subtasks, issue.project)
  473. end)
  474. safe_attributes(
  475. 'deleted_attachment_ids',
  476. :if => lambda {|issue, user| issue.attachments_deletable?(user)})
  477. def safe_attribute_names(user=nil)
  478. names = super
  479. names -= disabled_core_fields
  480. names -= read_only_attribute_names(user)
  481. if new_record?
  482. # Make sure that project_id can always be set for new issues
  483. names |= %w(project_id)
  484. end
  485. if dates_derived?
  486. names -= %w(start_date due_date)
  487. end
  488. if priority_derived?
  489. names -= %w(priority_id)
  490. end
  491. if done_ratio_derived?
  492. names -= %w(done_ratio)
  493. end
  494. names
  495. end
  496. # Safely sets attributes
  497. # Should be called from controllers instead of #attributes=
  498. # attr_accessible is too rough because we still want things like
  499. # Issue.new(:project => foo) to work
  500. def safe_attributes=(attrs, user=User.current)
  501. if attrs.respond_to?(:to_unsafe_hash)
  502. attrs = attrs.to_unsafe_hash
  503. end
  504. @attributes_set_by = user
  505. return unless attrs.is_a?(Hash)
  506. attrs = attrs.deep_dup
  507. # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
  508. if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
  509. if p.is_a?(String) && !/^\d*$/.match?(p)
  510. p_id = Project.find_by_identifier(p).try(:id)
  511. else
  512. p_id = p.to_i
  513. end
  514. if allowed_target_projects(user).where(:id => p_id).exists?
  515. self.project_id = p_id
  516. end
  517. if project_id_changed? && attrs['category_id'].present? && attrs['category_id'].to_s == category_id_was.to_s
  518. # Discard submitted category on previous project
  519. attrs.delete('category_id')
  520. end
  521. end
  522. if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
  523. if allowed_target_trackers(user).where(:id => t.to_i).exists?
  524. self.tracker_id = t
  525. end
  526. end
  527. if project && tracker.nil?
  528. # Set a default tracker to accept custom field values
  529. # even if tracker is not specified
  530. allowed_trackers = allowed_target_trackers(user)
  531. if attrs['parent_issue_id'].present?
  532. # If parent_issue_id is present, the first tracker for which this field
  533. # is not disabled is chosen as default
  534. self.tracker = allowed_trackers.detect {|t| t.core_fields.include?('parent_issue_id')}
  535. end
  536. self.tracker ||= allowed_trackers.first
  537. end
  538. statuses_allowed = new_statuses_allowed_to(user)
  539. if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
  540. if statuses_allowed.collect(&:id).include?(s.to_i)
  541. self.status_id = s
  542. end
  543. end
  544. if new_record? && !statuses_allowed.include?(status)
  545. self.status = statuses_allowed.first || default_status
  546. end
  547. if (u = attrs.delete('assigned_to_id')) && safe_attribute?('assigned_to_id')
  548. self.assigned_to_id = u
  549. end
  550. attrs = delete_unsafe_attributes(attrs, user)
  551. return if attrs.empty?
  552. if attrs['parent_issue_id'].present?
  553. s = attrs['parent_issue_id'].to_s
  554. unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
  555. @invalid_parent_issue_id = attrs.delete('parent_issue_id')
  556. end
  557. end
  558. if attrs['custom_field_values'].present?
  559. editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
  560. attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
  561. end
  562. if attrs['custom_fields'].present?
  563. editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
  564. attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
  565. end
  566. assign_attributes attrs
  567. end
  568. def disabled_core_fields
  569. tracker ? tracker.disabled_core_fields : []
  570. end
  571. # Returns the custom_field_values that can be edited by the given user
  572. def editable_custom_field_values(user=nil)
  573. read_only = read_only_attribute_names(user)
  574. visible_custom_field_values(user).reject do |value|
  575. read_only.include?(value.custom_field_id.to_s)
  576. end
  577. end
  578. # Returns the custom fields that can be edited by the given user
  579. def editable_custom_fields(user=nil)
  580. editable_custom_field_values(user).map(&:custom_field).uniq
  581. end
  582. # Returns the names of attributes that are read-only for user or the current user
  583. # For users with multiple roles, the read-only fields are the intersection of
  584. # read-only fields of each role
  585. # The result is an array of strings where sustom fields are represented with their ids
  586. #
  587. # Examples:
  588. # issue.read_only_attribute_names # => ['due_date', '2']
  589. # issue.read_only_attribute_names(user) # => []
  590. def read_only_attribute_names(user=nil)
  591. workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
  592. end
  593. # Returns the names of required attributes for user or the current user
  594. # For users with multiple roles, the required fields are the intersection of
  595. # required fields of each role
  596. # The result is an array of strings where sustom fields are represented with their ids
  597. #
  598. # Examples:
  599. # issue.required_attribute_names # => ['due_date', '2']
  600. # issue.required_attribute_names(user) # => []
  601. def required_attribute_names(user=nil)
  602. workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
  603. end
  604. # Returns true if the attribute is required for user
  605. def required_attribute?(name, user=nil)
  606. required_attribute_names(user).include?(name.to_s)
  607. end
  608. # Returns a hash of the workflow rule by attribute for the given user
  609. #
  610. # Examples:
  611. # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
  612. def workflow_rule_by_attribute(user=nil)
  613. return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
  614. roles = roles_for_workflow(user || User.current)
  615. return {} if roles.empty?
  616. result = {}
  617. workflow_permissions =
  618. WorkflowPermission.where(
  619. :tracker_id => tracker_id, :old_status_id => status_id,
  620. :role_id => roles.map(&:id)
  621. ).to_a
  622. if workflow_permissions.any?
  623. workflow_rules = workflow_permissions.inject({}) do |h, wp|
  624. h[wp.field_name] ||= {}
  625. h[wp.field_name][wp.role_id] = wp.rule
  626. h
  627. end
  628. fields_with_roles = {}
  629. IssueCustomField.where(:visible => false).
  630. joins(:roles).pluck(:id, "role_id").
  631. each do |field_id, role_id|
  632. fields_with_roles[field_id] ||= []
  633. fields_with_roles[field_id] << role_id
  634. end
  635. roles.each do |role|
  636. fields_with_roles.each do |field_id, role_ids|
  637. unless role_ids.include?(role.id)
  638. field_name = field_id.to_s
  639. workflow_rules[field_name] ||= {}
  640. workflow_rules[field_name][role.id] = 'readonly'
  641. end
  642. end
  643. end
  644. workflow_rules.each do |attr, rules|
  645. next if rules.size < roles.size
  646. uniq_rules = rules.values.uniq
  647. if uniq_rules.size == 1
  648. result[attr] = uniq_rules.first
  649. else
  650. result[attr] = 'required'
  651. end
  652. end
  653. end
  654. @workflow_rule_by_attribute = result if user.nil?
  655. result
  656. end
  657. private :workflow_rule_by_attribute
  658. def done_ratio
  659. if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
  660. status.default_done_ratio
  661. else
  662. read_attribute(:done_ratio)
  663. end
  664. end
  665. def self.use_status_for_done_ratio?
  666. Setting.issue_done_ratio == 'issue_status'
  667. end
  668. def self.use_field_for_done_ratio?
  669. Setting.issue_done_ratio == 'issue_field'
  670. end
  671. def validate_issue
  672. if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
  673. errors.add :due_date, :greater_than_start_date
  674. end
  675. if start_date && start_date_changed? && soonest_start && start_date < soonest_start
  676. errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
  677. end
  678. if project && fixed_version_id
  679. if fixed_version.nil? || assignable_versions.exclude?(fixed_version)
  680. errors.add :fixed_version_id, :inclusion
  681. elsif reopening? && fixed_version.closed?
  682. errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
  683. end
  684. end
  685. if project && category_id
  686. unless project.issue_category_ids.include?(category_id)
  687. errors.add :category_id, :inclusion
  688. end
  689. end
  690. # Checks that the issue can not be added/moved to a disabled tracker
  691. if project && (tracker_id_changed? || project_id_changed?)
  692. if tracker && !project.trackers.include?(tracker)
  693. errors.add :tracker_id, :inclusion
  694. end
  695. end
  696. if project && assigned_to_id_changed? && assigned_to_id.present?
  697. unless assignable_users.include?(assigned_to)
  698. errors.add :assigned_to_id, :invalid
  699. end
  700. end
  701. # Checks parent issue assignment
  702. if @invalid_parent_issue_id.present?
  703. errors.add :parent_issue_id, :invalid
  704. elsif @parent_issue
  705. if !valid_parent_project?(@parent_issue)
  706. errors.add :parent_issue_id, :invalid
  707. elsif (@parent_issue != parent) && (
  708. self.would_reschedule?(@parent_issue) ||
  709. @parent_issue.self_and_ancestors.any? do |a|
  710. a.relations_from.any? do |r|
  711. r.relation_type == IssueRelation::TYPE_PRECEDES &&
  712. r.issue_to.would_reschedule?(self)
  713. end
  714. end
  715. )
  716. errors.add :parent_issue_id, :invalid
  717. elsif !closed? && @parent_issue.closed?
  718. # cannot attach an open issue to a closed parent
  719. errors.add :base, :open_issue_with_closed_parent
  720. elsif !new_record?
  721. # moving an existing issue
  722. if move_possible?(@parent_issue)
  723. # move accepted
  724. else
  725. errors.add :parent_issue_id, :invalid
  726. end
  727. end
  728. end
  729. end
  730. # Validates the issue against additional workflow requirements
  731. def validate_required_fields
  732. user = new_record? ? author : current_journal.try(:user)
  733. required_attribute_names(user).each do |attribute|
  734. if /^\d+$/.match?(attribute)
  735. attribute = attribute.to_i
  736. v = custom_field_values.detect {|v| v.custom_field_id == attribute}
  737. if v && Array(v.value).detect(&:present?).nil?
  738. errors.add(v.custom_field.name, l('activerecord.errors.messages.blank'))
  739. end
  740. else
  741. if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
  742. next if attribute == 'category_id' && project.try(:issue_categories).blank?
  743. next if attribute == 'fixed_version_id' && assignable_versions.blank?
  744. errors.add attribute, :blank
  745. end
  746. end
  747. end
  748. end
  749. def validate_permissions
  750. if @attributes_set_by && new_record? && copy?
  751. unless allowed_target_trackers(@attributes_set_by).include?(tracker)
  752. errors.add :tracker, :invalid
  753. end
  754. end
  755. end
  756. # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
  757. # so that custom values that are not editable are not validated (eg. a custom field that
  758. # is marked as required should not trigger a validation error if the user is not allowed
  759. # to edit this field).
  760. def validate_custom_field_values
  761. user = new_record? ? author : current_journal.try(:user)
  762. if new_record? || custom_field_values_changed?
  763. editable_custom_field_values(user).each(&:validate_value)
  764. end
  765. end
  766. # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
  767. # even if the user turns off the setting later
  768. def update_done_ratio_from_issue_status
  769. if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
  770. self.done_ratio = status.default_done_ratio
  771. end
  772. end
  773. def init_journal(user, notes = "")
  774. @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
  775. end
  776. # Returns the current journal or nil if it's not initialized
  777. def current_journal
  778. @current_journal
  779. end
  780. # Clears the current journal
  781. def clear_journal
  782. @current_journal = nil
  783. end
  784. # Returns the names of attributes that are journalized when updating the issue
  785. def journalized_attribute_names
  786. names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
  787. if tracker
  788. names -= tracker.disabled_core_fields
  789. end
  790. names
  791. end
  792. # Returns the id of the last journal or nil
  793. def last_journal_id
  794. if new_record?
  795. nil
  796. else
  797. journals.maximum(:id)
  798. end
  799. end
  800. # Returns a scope for journals that have an id greater than journal_id
  801. def journals_after(journal_id)
  802. scope = journals.reorder("#{Journal.table_name}.id ASC")
  803. if journal_id.present?
  804. scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
  805. end
  806. scope
  807. end
  808. # Returns the journals that are visible to user with their index
  809. # Used to display the issue history
  810. def visible_journals_with_index(user=User.current)
  811. result = journals.
  812. preload(:details).
  813. preload(:user => :email_address).
  814. reorder(:created_on, :id).to_a
  815. result.each_with_index {|j, i| j.indice = i + 1}
  816. unless user.allowed_to?(:view_private_notes, project)
  817. result.select! do |journal|
  818. !journal.private_notes? || journal.user == user
  819. end
  820. end
  821. Journal.preload_journals_details_custom_fields(result)
  822. result.select! {|journal| journal.notes? || journal.visible_details.any?}
  823. result
  824. end
  825. # Returns the initial status of the issue
  826. # Returns nil for a new issue
  827. def status_was
  828. if status_id_changed?
  829. if status_id_was.to_i > 0
  830. @status_was ||= IssueStatus.find_by_id(status_id_was)
  831. end
  832. else
  833. @status_was ||= status
  834. end
  835. end
  836. # Return true if the issue is closed, otherwise false
  837. def closed?
  838. status.present? && status.is_closed?
  839. end
  840. # Returns true if the issue was closed when loaded
  841. def was_closed?
  842. status_was.present? && status_was.is_closed?
  843. end
  844. # Return true if the issue is being reopened
  845. def reopening?
  846. if new_record?
  847. false
  848. else
  849. status_id_changed? && !closed? && was_closed?
  850. end
  851. end
  852. alias :reopened? :reopening?
  853. # Return true if the issue is being closed
  854. def closing?
  855. if new_record?
  856. closed?
  857. else
  858. status_id_changed? && closed? && !was_closed?
  859. end
  860. end
  861. # Returns true if the issue is overdue
  862. def overdue?
  863. due_date.present? && (due_date < User.current.today) && !closed?
  864. end
  865. # Is the amount of work done less than it should for the due date
  866. def behind_schedule?
  867. return false if start_date.nil? || due_date.nil?
  868. done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
  869. return done_date <= User.current.today
  870. end
  871. # Does this issue have children?
  872. def children?
  873. !leaf?
  874. end
  875. # Users the issue can be assigned to
  876. def assignable_users
  877. return [] if project.nil?
  878. users = project.assignable_users(tracker).to_a
  879. users << author if author && author.active?
  880. if assigned_to_id_was.present? && assignee = Principal.find_by_id(assigned_to_id_was)
  881. users << assignee
  882. end
  883. users.uniq.sort
  884. end
  885. # Versions that the issue can be assigned to
  886. def assignable_versions
  887. return @assignable_versions if @assignable_versions
  888. return [] if project.nil?
  889. versions = project.shared_versions.open.to_a
  890. if fixed_version
  891. if fixed_version_id_changed?
  892. # nothing to do
  893. elsif project_id_changed?
  894. if project.shared_versions.include?(fixed_version)
  895. versions << fixed_version
  896. end
  897. else
  898. versions << fixed_version
  899. end
  900. end
  901. @assignable_versions = versions.uniq.sort
  902. end
  903. # Returns true if this issue is blocked by another issue that is still open
  904. def blocked?
  905. !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
  906. end
  907. # Returns true if this issue can be closed and if not, returns false and populates the reason
  908. def closable?
  909. if descendants.open.any?
  910. @transition_warning = l(:notice_issue_not_closable_by_open_tasks)
  911. return false
  912. end
  913. if blocked?
  914. @transition_warning = l(:notice_issue_not_closable_by_blocking_issue)
  915. return false
  916. end
  917. return true
  918. end
  919. # Returns true if this issue can be reopen and if not, returns false and populates the reason
  920. def reopenable?
  921. if ancestors.open(false).any?
  922. @transition_warning = l(:notice_issue_not_reopenable_by_closed_parent_issue)
  923. return false
  924. end
  925. return true
  926. end
  927. # Returns the default status of the issue based on its tracker
  928. # Returns nil if tracker is nil
  929. def default_status
  930. tracker.try(:default_status)
  931. end
  932. # Returns an array of statuses that user is able to apply
  933. def new_statuses_allowed_to(user=User.current, include_default=false)
  934. initial_status = nil
  935. if new_record?
  936. # nop
  937. elsif tracker_id_changed?
  938. if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
  939. initial_status = default_status
  940. elsif tracker.issue_status_ids.include?(status_id_was)
  941. initial_status = IssueStatus.find_by_id(status_id_was)
  942. else
  943. initial_status = default_status
  944. end
  945. else
  946. initial_status = status_was
  947. end
  948. initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
  949. assignee_transitions_allowed = initial_assigned_to_id.present? &&
  950. (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
  951. statuses = []
  952. statuses += IssueStatus.new_statuses_allowed(
  953. initial_status,
  954. roles_for_workflow(user),
  955. tracker,
  956. author == user,
  957. assignee_transitions_allowed
  958. )
  959. statuses << initial_status unless statuses.empty?
  960. statuses << default_status if include_default || (new_record? && statuses.empty?)
  961. statuses = statuses.compact.uniq.sort
  962. unless closable?
  963. # cannot close a blocked issue or a parent with open subtasks
  964. statuses.reject!(&:is_closed?)
  965. end
  966. unless reopenable?
  967. # cannot reopen a subtask of a closed parent
  968. statuses.select!(&:is_closed?)
  969. end
  970. statuses
  971. end
  972. # Returns the original tracker
  973. def tracker_was
  974. Tracker.find_by_id(tracker_id_in_database)
  975. end
  976. # Returns the previous assignee whenever we're before the save
  977. # or in after_* callbacks
  978. def previous_assignee
  979. previous_assigned_to_id =
  980. if assigned_to_id_change_to_be_saved.nil?
  981. assigned_to_id_before_last_save
  982. else
  983. assigned_to_id_in_database
  984. end
  985. if previous_assigned_to_id
  986. Principal.find_by_id(previous_assigned_to_id)
  987. end
  988. end
  989. # Returns the users that should be notified
  990. def notified_users
  991. # Author and assignee are always notified unless they have been
  992. # locked or don't want to be notified
  993. notified = [author, assigned_to, previous_assignee].compact.uniq
  994. notified = notified.map {|n| n.is_a?(Group) ? n.users : n}.flatten
  995. notified.uniq!
  996. notified = notified.select {|u| u.active? && u.notify_about?(self)}
  997. notified += project.notified_users
  998. notified += project.users.preload(:preference).select(&:notify_about_high_priority_issues?) if priority.high?
  999. notified.uniq!
  1000. # Remove users that can not view the issue
  1001. notified.reject! {|user| !visible?(user)}
  1002. notified
  1003. end
  1004. # Returns the email addresses that should be notified
  1005. def recipients
  1006. notified_users.collect(&:mail)
  1007. end
  1008. def notify?
  1009. @notify != false
  1010. end
  1011. def notify=(arg)
  1012. @notify = arg
  1013. end
  1014. # Returns the number of hours spent on this issue
  1015. def spent_hours
  1016. @spent_hours ||= time_entries.sum(:hours) || 0.0
  1017. end
  1018. # Returns the total number of hours spent on this issue and its descendants
  1019. def total_spent_hours
  1020. @total_spent_hours ||=
  1021. if leaf?
  1022. spent_hours
  1023. else
  1024. self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
  1025. end
  1026. end
  1027. def total_estimated_hours
  1028. if leaf?
  1029. estimated_hours
  1030. else
  1031. @total_estimated_hours ||= self_and_descendants.visible.sum(:estimated_hours)
  1032. end
  1033. end
  1034. def relations
  1035. @relations ||= IssueRelation::Relations.new(
  1036. self,
  1037. IssueRelation.where('issue_from_id = ? OR issue_to_id = ?', id, id).sort
  1038. )
  1039. end
  1040. def last_updated_by
  1041. if @last_updated_by
  1042. @last_updated_by.presence
  1043. else
  1044. journals.reorder(:id => :desc).first.try(:user)
  1045. end
  1046. end
  1047. def last_notes
  1048. if @last_notes
  1049. @last_notes
  1050. else
  1051. journals.visible.where.not(notes: '').reorder(:id => :desc).first.try(:notes)
  1052. end
  1053. end
  1054. # Preloads relations for a collection of issues
  1055. def self.load_relations(issues)
  1056. if issues.any?
  1057. relations =
  1058. IssueRelation.where(
  1059. "issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)
  1060. ).all
  1061. issues.each do |issue|
  1062. issue.instance_variable_set(
  1063. :@relations,
  1064. relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
  1065. )
  1066. end
  1067. end
  1068. end
  1069. # Preloads visible spent time for a collection of issues
  1070. def self.load_visible_spent_hours(issues, user=User.current)
  1071. if issues.any?
  1072. hours_by_issue_id = TimeEntry.visible(user).where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours)
  1073. issues.each do |issue|
  1074. issue.instance_variable_set :@spent_hours, (hours_by_issue_id[issue.id] || 0.0)
  1075. end
  1076. end
  1077. end
  1078. # Preloads visible total spent time for a collection of issues
  1079. def self.load_visible_total_spent_hours(issues, user=User.current)
  1080. if issues.any?
  1081. hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
  1082. joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
  1083. " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
  1084. where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
  1085. issues.each do |issue|
  1086. issue.instance_variable_set :@total_spent_hours, (hours_by_issue_id[issue.id] || 0.0)
  1087. end
  1088. end
  1089. end
  1090. # Preloads visible relations for a collection of issues
  1091. def self.load_visible_relations(issues, user=User.current)
  1092. if issues.any?
  1093. issue_ids = issues.map(&:id)
  1094. # Relations with issue_from in given issues and visible issue_to
  1095. relations_from = IssueRelation.joins(:issue_to => :project).
  1096. where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
  1097. # Relations with issue_to in given issues and visible issue_from
  1098. relations_to = IssueRelation.joins(:issue_from => :project).
  1099. where(visible_condition(user)).
  1100. where(:issue_to_id => issue_ids).to_a
  1101. issues.each do |issue|
  1102. relations =
  1103. relations_from.select {|relation| relation.issue_from_id == issue.id} +
  1104. relations_to.select {|relation| relation.issue_to_id == issue.id}
  1105. issue.instance_variable_set :@relations, IssueRelation::Relations.new(issue, relations.sort)
  1106. end
  1107. end
  1108. end
  1109. # Returns a scope of the given issues and their descendants
  1110. def self.self_and_descendants(issues)
  1111. Issue.joins(
  1112. "JOIN #{Issue.table_name} ancestors" +
  1113. " ON ancestors.root_id = #{Issue.table_name}.root_id" +
  1114. " AND ancestors.lft <= #{Issue.table_name}.lft AND ancestors.rgt >= #{Issue.table_name}.rgt"
  1115. ).
  1116. where(:ancestors => {:id => issues.map(&:id)})
  1117. end
  1118. # Preloads users who updated last a collection of issues
  1119. def self.load_visible_last_updated_by(issues, user=User.current)
  1120. if issues.any?
  1121. issue_ids = issues.map(&:id)
  1122. journal_ids = Journal.joins(issue: :project).
  1123. where(:journalized_type => 'Issue', :journalized_id => issue_ids).
  1124. where(Journal.visible_notes_condition(user, :skip_pre_condition => true)).
  1125. group(:journalized_id).
  1126. maximum(:id).
  1127. values
  1128. journals = Journal.where(:id => journal_ids).preload(:user).to_a
  1129. issues.each do |issue|
  1130. journal = journals.detect {|j| j.journalized_id == issue.id}
  1131. issue.instance_variable_set(:@last_updated_by, journal.try(:user) || '')
  1132. end
  1133. end
  1134. end
  1135. # Preloads visible last notes for a collection of issues
  1136. def self.load_visible_last_notes(issues, user=User.current)
  1137. if issues.any?
  1138. issue_ids = issues.map(&:id)
  1139. journal_ids = Journal.joins(issue: :project).
  1140. where(:journalized_type => 'Issue', :journalized_id => issue_ids).
  1141. where(Journal.visible_notes_condition(user, :skip_pre_condition => true)).
  1142. where.not(notes: '').
  1143. group(:journalized_id).
  1144. maximum(:id).
  1145. values
  1146. journals = Journal.where(:id => journal_ids).to_a
  1147. issues.each do |issue|
  1148. journal = journals.detect {|j| j.journalized_id == issue.id}
  1149. issue.instance_variable_set(:@last_notes, journal.try(:notes) || '')
  1150. end
  1151. end
  1152. end
  1153. # Finds an issue relation given its id.
  1154. def find_relation(relation_id)
  1155. IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
  1156. end
  1157. # Returns true if this issue blocks the other issue, otherwise returns false
  1158. def blocks?(other)
  1159. all = [self]
  1160. last = [self]
  1161. while last.any?
  1162. current =
  1163. last.map do |i|
  1164. i.relations_from.where(:relation_type => IssueRelation::TYPE_BLOCKS).map(&:issue_to)
  1165. end.flatten.uniq
  1166. current -= last
  1167. current -= all
  1168. return true if current.include?(other)
  1169. last = current
  1170. all += last
  1171. end
  1172. false
  1173. end
  1174. # Returns true if the other issue might be rescheduled if the start/due dates of this issue change
  1175. def would_reschedule?(other)
  1176. all = [self]
  1177. last = [self]
  1178. while last.any?
  1179. current = last.map do |i|
  1180. i.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to) +
  1181. i.leaves.to_a +
  1182. i.ancestors.map {|a| a.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to)}
  1183. end.flatten.uniq
  1184. current -= last
  1185. current -= all
  1186. return true if current.include?(other)
  1187. last = current
  1188. all += last
  1189. end
  1190. false
  1191. end
  1192. # Returns an array of issues that duplicate this one
  1193. def duplicates
  1194. relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
  1195. end
  1196. # Returns the due date or the target due date if any
  1197. # Used on gantt chart
  1198. def due_before
  1199. due_date || (fixed_version ? fixed_version.effective_date : nil)
  1200. end
  1201. # Returns the time scheduled for this issue.
  1202. #
  1203. # Example:
  1204. # Start Date: 2/26/09, End Date: 3/04/09
  1205. # duration => 6
  1206. def duration
  1207. (start_date && due_date) ? due_date - start_date : 0
  1208. end
  1209. # Returns the duration in working days
  1210. def working_duration
  1211. (start_date && due_date) ? working_days(start_date, due_date) : 0
  1212. end
  1213. def soonest_start(reload=false)
  1214. if @soonest_start.nil? || reload
  1215. relations_to.reload if reload
  1216. dates = relations_to.collect{|relation| relation.successor_soonest_start}
  1217. p = @parent_issue || parent
  1218. if p && Setting.parent_issue_dates == 'derived'
  1219. dates << p.soonest_start
  1220. end
  1221. @soonest_start = dates.compact.max
  1222. end
  1223. @soonest_start
  1224. end
  1225. # Sets start_date on the given date or the next working day
  1226. # and changes due_date to keep the same working duration.
  1227. def reschedule_on(date)
  1228. wd = working_duration
  1229. date = next_working_date(date)
  1230. self.start_date = date
  1231. self.due_date = add_working_days(date, wd)
  1232. end
  1233. # Reschedules the issue on the given date or the next working day and saves the record.
  1234. # If the issue is a parent task, this is done by rescheduling its subtasks.
  1235. def reschedule_on!(date, journal=nil)
  1236. return if date.nil?
  1237. if leaf? || !dates_derived?
  1238. if start_date.nil? || start_date != date
  1239. if start_date && start_date > date
  1240. # Issue can not be moved earlier than its soonest start date
  1241. date = [soonest_start(true), date].compact.max
  1242. end
  1243. if journal
  1244. init_journal(journal.user)
  1245. end
  1246. reschedule_on(date)
  1247. begin
  1248. save
  1249. rescue ActiveRecord::StaleObjectError
  1250. reload
  1251. reschedule_on(date)
  1252. save
  1253. end
  1254. end
  1255. else
  1256. leaves.each do |leaf|
  1257. if leaf.start_date
  1258. # Only move subtask if it starts at the same date as the parent
  1259. # or if it starts before the given date
  1260. if start_date == leaf.start_date || date > leaf.start_date
  1261. leaf.reschedule_on!(date)
  1262. end
  1263. else
  1264. leaf.reschedule_on!(date)
  1265. end
  1266. end
  1267. end
  1268. end
  1269. def dates_derived?
  1270. !leaf? && Setting.parent_issue_dates == 'derived'
  1271. end
  1272. def priority_derived?
  1273. !leaf? && Setting.parent_issue_priority == 'derived'
  1274. end
  1275. def done_ratio_derived?
  1276. !leaf? && Setting.parent_issue_done_ratio == 'derived'
  1277. end
  1278. def <=>(issue)
  1279. return nil unless issue.is_a?(Issue)
  1280. if root_id != issue.root_id
  1281. (root_id || 0) <=> (issue.root_id || 0)
  1282. else
  1283. (lft || 0) <=> (issue.lft || 0)
  1284. end
  1285. end
  1286. def to_s
  1287. "#{tracker} ##{id}: #{subject}"
  1288. end
  1289. # Returns a string of css classes that apply to the issue
  1290. def css_classes(user=User.current)
  1291. s = +"issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
  1292. s << ' closed' if closed?
  1293. s << ' overdue' if overdue?
  1294. s << ' child' if child?
  1295. s << ' parent' unless leaf?
  1296. s << ' private' if is_private?
  1297. s << ' behind-schedule' if behind_schedule?
  1298. if user.logged?
  1299. s << ' created-by-me' if author_id == user.id
  1300. s << ' assigned-to-me' if assigned_to_id == user.id
  1301. s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
  1302. end
  1303. s
  1304. end
  1305. # Unassigns issues from +version+ if it's no longer shared with issue's project
  1306. def self.update_versions_from_sharing_change(version)
  1307. # Update issues assigned to the version
  1308. update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
  1309. end
  1310. # Unassigns issues from versions that are no longer shared
  1311. # after +project+ was moved
  1312. def self.update_versions_from_hierarchy_change(project)
  1313. moved_project_ids = project.self_and_descendants.reload.pluck(:id)
  1314. # Update issues of the moved projects and issues assigned to a version of a moved project
  1315. Issue.
  1316. update_versions(
  1317. ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
  1318. moved_project_ids, moved_project_ids]
  1319. )
  1320. end
  1321. def parent_issue_id=(arg)
  1322. s = arg.to_s.strip.presence
  1323. if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
  1324. @invalid_parent_issue_id = nil
  1325. elsif s.blank?
  1326. @parent_issue = nil
  1327. @invalid_parent_issue_id = nil
  1328. else
  1329. @parent_issue = nil
  1330. @invalid_parent_issue_id = arg
  1331. end
  1332. end
  1333. def parent_issue_id
  1334. if @invalid_parent_issue_id
  1335. @invalid_parent_issue_id
  1336. elsif instance_variable_defined? :@parent_issue
  1337. @parent_issue.nil? ? nil : @parent_issue.id
  1338. else
  1339. parent_id
  1340. end
  1341. end
  1342. alias :parent_issue :parent
  1343. def set_parent_id
  1344. self.parent_id = parent_issue_id
  1345. end
  1346. # Returns true if issue's project is a valid
  1347. # parent issue project
  1348. def valid_parent_project?(issue=parent)
  1349. return true if issue.nil? || issue.project_id == project_id
  1350. case Setting.cross_project_subtasks
  1351. when 'system'
  1352. true
  1353. when 'tree'
  1354. issue.project.root == project.root
  1355. when 'hierarchy'
  1356. issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
  1357. when 'descendants'
  1358. issue.project.is_or_is_ancestor_of?(project)
  1359. else
  1360. false
  1361. end
  1362. end
  1363. # Returns an issue scope based on project and scope
  1364. def self.cross_project_scope(project, scope=nil)
  1365. if project.nil?
  1366. return Issue
  1367. end
  1368. case scope
  1369. when 'all', 'system'
  1370. Issue
  1371. when 'tree'
  1372. Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
  1373. :lft => project.root.lft, :rgt => project.root.rgt)
  1374. when 'hierarchy'
  1375. Issue.joins(:project).
  1376. where(
  1377. "(#{Project.table_name}.lft >= :lft AND " \
  1378. "#{Project.table_name}.rgt <= :rgt) OR " \
  1379. "(#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
  1380. :lft => project.lft, :rgt => project.rgt
  1381. )
  1382. when 'descendants'
  1383. Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
  1384. :lft => project.lft, :rgt => project.rgt)
  1385. else
  1386. Issue.where(:project_id => project.id)
  1387. end
  1388. end
  1389. def self.by_tracker(project, with_subprojects=false)
  1390. count_and_group_by(:project => project, :association => :tracker, :with_subprojects => with_subprojects)
  1391. end
  1392. def self.by_version(project, with_subprojects=false)
  1393. count_and_group_by(:project => project, :association => :fixed_version, :with_subprojects => with_subprojects)
  1394. end
  1395. def self.by_priority(project, with_subprojects=false)
  1396. count_and_group_by(:project => project, :association => :priority, :with_subprojects => with_subprojects)
  1397. end
  1398. def self.by_category(project, with_subprojects=false)
  1399. count_and_group_by(:project => project, :association => :category, :with_subprojects => with_subprojects)
  1400. end
  1401. def self.by_assigned_to(project, with_subprojects=false)
  1402. count_and_group_by(:project => project, :association => :assigned_to, :with_subprojects => with_subprojects)
  1403. end
  1404. def self.by_author(project, with_subprojects=false)
  1405. count_and_group_by(:project => project, :association => :author, :with_subprojects => with_subprojects)
  1406. end
  1407. def self.by_subproject(project)
  1408. r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
  1409. r.reject {|r| r["project_id"] == project.id.to_s}
  1410. end
  1411. # Query generator for selecting groups of issue counts for a project
  1412. # based on specific criteria
  1413. #
  1414. # Options
  1415. # * project - Project to search in.
  1416. # * with_subprojects - Includes subprojects issues if set to true.
  1417. # * association - Symbol. Association for grouping.
  1418. def self.count_and_group_by(options)
  1419. assoc = reflect_on_association(options[:association])
  1420. select_field = assoc.foreign_key
  1421. Issue.
  1422. visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
  1423. joins(:status).
  1424. group(:status_id, :is_closed, select_field).
  1425. count.
  1426. map do |columns, total|
  1427. status_id, is_closed, field_value = columns
  1428. is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
  1429. {
  1430. "status_id" => status_id.to_s,
  1431. "closed" => is_closed,
  1432. select_field => field_value.to_s,
  1433. "total" => total.to_s
  1434. }
  1435. end
  1436. end
  1437. # Returns a scope of projects that user can assign the subtask
  1438. def allowed_target_projects_for_subtask(user=User.current)
  1439. if parent_issue_id.present?
  1440. scope = filter_projects_scope(Setting.cross_project_subtasks)
  1441. end
  1442. self.class.allowed_target_projects(user, project, scope)
  1443. end
  1444. # Returns a scope of projects that user can assign the issue to
  1445. def allowed_target_projects(user=User.current, scope=nil)
  1446. current_project = new_record? ? nil : project
  1447. if scope
  1448. scope = filter_projects_scope(scope)
  1449. end
  1450. self.class.allowed_target_projects(user, current_project, scope)
  1451. end
  1452. # Returns a scope of projects that user can assign issues to
  1453. # If current_project is given, it will be included in the scope
  1454. def self.allowed_target_projects(user=User.current, current_project=nil, scope=nil)
  1455. condition = Project.allowed_to_condition(user, :add_issues)
  1456. if current_project
  1457. condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
  1458. end
  1459. if scope.nil?
  1460. scope = Project
  1461. end
  1462. scope.where(condition).having_trackers
  1463. end
  1464. # Returns a scope of trackers that user can assign the issue to
  1465. def allowed_target_trackers(user=User.current)
  1466. self.class.allowed_target_trackers(project, user, tracker_id_was)
  1467. end
  1468. # Returns a scope of trackers that user can assign project issues to
  1469. def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
  1470. if project
  1471. scope = project.trackers.sorted
  1472. unless user.admin?
  1473. roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
  1474. unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
  1475. tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq
  1476. if current_tracker
  1477. tracker_ids << current_tracker
  1478. end
  1479. scope = scope.where(:id => tracker_ids)
  1480. end
  1481. end
  1482. scope
  1483. else
  1484. Tracker.none
  1485. end
  1486. end
  1487. private
  1488. def user_tracker_permission?(user, permission)
  1489. if project && !project.active?
  1490. perm = Redmine::AccessControl.permission(permission)
  1491. return false unless perm && perm.read?
  1492. end
  1493. if user.admin?
  1494. true
  1495. else
  1496. roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)}
  1497. roles.any? do |r|
  1498. r.permissions_all_trackers?(permission) ||
  1499. r.permissions_tracker_ids?(permission, tracker_id)
  1500. end
  1501. end
  1502. end
  1503. def after_project_change
  1504. # Update project_id on related time entries
  1505. TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
  1506. # Delete issue relations
  1507. unless Setting.cross_project_issue_relations?
  1508. relations_from.clear
  1509. relations_to.clear
  1510. end
  1511. # Move subtasks that were in the same project
  1512. children.each do |child|
  1513. next unless child.project_id == project_id_before_last_save
  1514. # Change project and keep project
  1515. child.send :project=, project, true
  1516. unless child.save
  1517. errors.add(
  1518. :base,
  1519. l(:error_move_of_child_not_possible,
  1520. :child => "##{child.id}",
  1521. :errors => child.errors.full_messages.join(", "))
  1522. )
  1523. raise ActiveRecord::Rollback
  1524. end
  1525. end
  1526. end
  1527. # Callback for after the creation of an issue by copy
  1528. # * adds a "copied to" relation with the copied issue
  1529. # * copies subtasks from the copied issue
  1530. def after_create_from_copy
  1531. return unless copy? && !@after_create_from_copy_handled
  1532. if (@copied_from.project_id == project_id ||
  1533. Setting.cross_project_issue_relations?) &&
  1534. @copy_options[:link] != false
  1535. if @current_journal
  1536. @copied_from.init_journal(@current_journal.user)
  1537. end
  1538. relation =
  1539. IssueRelation.new(:issue_from => @copied_from, :issue_to => self,
  1540. :relation_type => IssueRelation::TYPE_COPIED_TO)
  1541. unless relation.save
  1542. if logger
  1543. logger.error(
  1544. "Could not create relation while copying ##{@copied_from.id} to ##{id} " \
  1545. "due to validation errors: #{relation.errors.full_messages.join(', ')}"
  1546. )
  1547. end
  1548. end
  1549. end
  1550. unless @copied_from.leaf? || @copy_options[:subtasks] == false
  1551. copy_options = (@copy_options || {}).merge(:subtasks => false)
  1552. copied_issue_ids = {@copied_from.id => self.id}
  1553. @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
  1554. # Do not copy self when copying an issue as a descendant of the copied issue
  1555. next if child == self
  1556. # Do not copy subtasks of issues that were not copied
  1557. next unless copied_issue_ids[child.parent_id]
  1558. # Do not copy subtasks that are not visible to avoid potential disclosure of private data
  1559. unless child.visible?
  1560. if logger
  1561. logger.error(
  1562. "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy " \
  1563. "because it is not visible to the current user"
  1564. )
  1565. end
  1566. next
  1567. end
  1568. copy = Issue.new.copy_from(child, copy_options)
  1569. if @current_journal
  1570. copy.init_journal(@current_journal.user)
  1571. end
  1572. copy.author = author
  1573. copy.project = project
  1574. copy.parent_issue_id = copied_issue_ids[child.parent_id]
  1575. unless child.fixed_version.present? && child.fixed_version.status == 'open'
  1576. copy.fixed_version_id = nil
  1577. end
  1578. unless child.assigned_to_id.present? &&
  1579. child.assigned_to.status == User::STATUS_ACTIVE
  1580. copy.assigned_to = nil
  1581. end
  1582. unless copy.save
  1583. if logger
  1584. logger.error(
  1585. "Could not copy subtask ##{child.id} " \
  1586. "while copying ##{@copied_from.id} to ##{id} due to validation errors: " \
  1587. "#{copy.errors.full_messages.join(', ')}"
  1588. )
  1589. end
  1590. next
  1591. end
  1592. copied_issue_ids[child.id] = copy.id
  1593. end
  1594. end
  1595. @after_create_from_copy_handled = true
  1596. end
  1597. def update_nested_set_attributes
  1598. if saved_change_to_parent_id?
  1599. update_nested_set_attributes_on_parent_change
  1600. end
  1601. remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
  1602. end
  1603. # Updates the nested set for when an existing issue is moved
  1604. def update_nested_set_attributes_on_parent_change
  1605. former_parent_id = parent_id_before_last_save
  1606. # delete invalid relations of all descendants
  1607. self_and_descendants.each do |issue|
  1608. issue.relations.each do |relation|
  1609. relation.destroy unless relation.valid?
  1610. end
  1611. end
  1612. # update former parent
  1613. recalculate_attributes_for(former_parent_id) if former_parent_id
  1614. end
  1615. def update_parent_attributes
  1616. if parent_id
  1617. recalculate_attributes_for(parent_id)
  1618. association(:parent).reset
  1619. end
  1620. end
  1621. def recalculate_attributes_for(issue_id)
  1622. if issue_id && p = Issue.find_by_id(issue_id)
  1623. if p.priority_derived?
  1624. # priority = highest priority of open children
  1625. # priority is left unchanged if all children are closed and there's no default priority defined
  1626. if priority_position =
  1627. p.children.open.joins(:priority).maximum("#{IssuePriority.table_name}.position")
  1628. p.priority = IssuePriority.find_by_position(priority_position)
  1629. elsif default_priority = IssuePriority.default
  1630. p.priority = default_priority
  1631. end
  1632. end
  1633. if p.dates_derived?
  1634. # start/due dates = lowest/highest dates of children
  1635. p.start_date = p.children.minimum(:start_date)
  1636. p.due_date = p.children.maximum(:due_date)
  1637. if p.start_date && p.due_date && p.due_date < p.start_date
  1638. p.start_date, p.due_date = p.due_date, p.start_date
  1639. end
  1640. end
  1641. if p.done_ratio_derived?
  1642. # done ratio = average ratio of children weighted with their total estimated hours
  1643. unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
  1644. children = p.children.to_a
  1645. if children.any?
  1646. child_with_total_estimated_hours = children.select {|c| c.total_estimated_hours.to_f > 0.0}
  1647. if child_with_total_estimated_hours.any?
  1648. average = Rational(
  1649. child_with_total_estimated_hours.sum(&:total_estimated_hours).to_s,
  1650. child_with_total_estimated_hours.count
  1651. )
  1652. else
  1653. average = Rational(1)
  1654. end
  1655. done = children.sum do |c|
  1656. estimated = Rational(c.total_estimated_hours.to_f.to_s)
  1657. estimated = average unless estimated > 0.0
  1658. ratio = c.closed? ? 100 : (c.done_ratio || 0)
  1659. estimated * ratio
  1660. end
  1661. progress = Rational(done, average * children.count)
  1662. p.done_ratio = progress.floor
  1663. end
  1664. end
  1665. end
  1666. # ancestors will be recursively updated
  1667. p.save(:validate => false)
  1668. end
  1669. end
  1670. # Singleton class method is public
  1671. class << self
  1672. # Update issues so their versions are not pointing to a
  1673. # fixed_version that is not shared with the issue's project
  1674. def update_versions(conditions=nil)
  1675. # Only need to update issues with a fixed_version from
  1676. # a different project and that is not systemwide shared
  1677. Issue.joins(:project, :fixed_version).
  1678. where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
  1679. " AND #{Issue.table_name}.project_id <> #{::Version.table_name}.project_id" +
  1680. " AND #{::Version.table_name}.sharing <> 'system'").
  1681. where(conditions).each do |issue|
  1682. next if issue.project.nil? || issue.fixed_version.nil?
  1683. unless issue.project.shared_versions.include?(issue.fixed_version)
  1684. retried = false
  1685. begin
  1686. issue.init_journal(User.current)
  1687. issue.fixed_version = nil
  1688. issue.save
  1689. rescue ActiveRecord::StaleObjectError
  1690. raise if retried
  1691. retried = true
  1692. issue.reload
  1693. retry
  1694. end
  1695. end
  1696. end
  1697. end
  1698. end
  1699. def delete_selected_attachments
  1700. if deleted_attachment_ids.present?
  1701. objects = attachments.where(:id => deleted_attachment_ids.map(&:to_i))
  1702. attachments.delete(objects)
  1703. end
  1704. end
  1705. # Callback on file attachment
  1706. def attachment_added(attachment)
  1707. if current_journal && !attachment.new_record? && !copy?
  1708. current_journal.journalize_attachment(attachment, :added)
  1709. end
  1710. end
  1711. # Callback on attachment deletion
  1712. def attachment_removed(attachment)
  1713. if current_journal && !attachment.new_record?
  1714. current_journal.journalize_attachment(attachment, :removed)
  1715. current_journal.save
  1716. end
  1717. end
  1718. # Called after a relation is added
  1719. def relation_added(relation)
  1720. if current_journal
  1721. current_journal.journalize_relation(relation, :added)
  1722. current_journal.save
  1723. end
  1724. end
  1725. # Called after a relation is removed
  1726. def relation_removed(relation)
  1727. if current_journal
  1728. current_journal.journalize_relation(relation, :removed)
  1729. current_journal.save
  1730. end
  1731. end
  1732. # Default assignment based on project or category
  1733. def default_assign
  1734. if assigned_to.nil?
  1735. if category && category.assigned_to
  1736. self.assigned_to = category.assigned_to
  1737. elsif project && project.default_assigned_to
  1738. self.assigned_to = project.default_assigned_to
  1739. end
  1740. end
  1741. end
  1742. # Updates start/due dates of following issues
  1743. def reschedule_following_issues
  1744. if saved_change_to_start_date? || saved_change_to_due_date?
  1745. relations_from.each do |relation|
  1746. relation.set_issue_to_dates(@current_journal)
  1747. end
  1748. end
  1749. end
  1750. # Closes duplicates if the issue is being closed
  1751. def close_duplicates
  1752. if Setting.close_duplicate_issues? && closing?
  1753. duplicates.each do |duplicate|
  1754. # Reload is needed in case the duplicate was updated by a previous duplicate
  1755. duplicate.reload
  1756. # Don't re-close it if it's already closed
  1757. next if duplicate.closed?
  1758. # Same user and notes
  1759. if @current_journal
  1760. duplicate.init_journal(@current_journal.user, @current_journal.notes)
  1761. duplicate.private_notes = @current_journal.private_notes
  1762. end
  1763. duplicate.update_attribute :status, self.status
  1764. end
  1765. end
  1766. end
  1767. # Make sure updated_on is updated when adding a note and set updated_on now
  1768. # so we can set closed_on with the same value on closing
  1769. def force_updated_on_change
  1770. if @current_journal || changed?
  1771. self.updated_on = current_time_from_proper_timezone
  1772. if new_record?
  1773. self.created_on = updated_on
  1774. end
  1775. end
  1776. end
  1777. # Callback for setting closed_on when the issue is closed.
  1778. # The closed_on attribute stores the time of the last closing
  1779. # and is preserved when the issue is reopened.
  1780. def update_closed_on
  1781. if closing?
  1782. self.closed_on = updated_on
  1783. end
  1784. end
  1785. # Saves the changes in a Journal
  1786. # Called after_save
  1787. def create_journal
  1788. if current_journal
  1789. current_journal.save
  1790. end
  1791. end
  1792. def create_parent_issue_journal
  1793. return if persisted? && !saved_change_to_parent_id?
  1794. return if destroyed? && @without_nested_set_update
  1795. child_id = self.id
  1796. old_parent_id, new_parent_id =
  1797. if persisted?
  1798. [parent_id_before_last_save, parent_id]
  1799. elsif destroyed?
  1800. [parent_id, nil]
  1801. else
  1802. [nil, parent_id]
  1803. end
  1804. if old_parent_id.present?
  1805. Issue.transaction do
  1806. if old_parent_issue = Issue.visible.lock.find_by_id(old_parent_id)
  1807. old_parent_issue.init_journal(User.current)
  1808. old_parent_issue.current_journal.__send__(:add_attribute_detail, 'child_id', child_id, nil)
  1809. old_parent_issue.save
  1810. end
  1811. end
  1812. end
  1813. if new_parent_id.present?
  1814. Issue.transaction do
  1815. if new_parent_issue = Issue.visible.lock.find_by_id(new_parent_id)
  1816. new_parent_issue.init_journal(User.current)
  1817. new_parent_issue.current_journal.__send__(:add_attribute_detail, 'child_id', nil, child_id)
  1818. new_parent_issue.save
  1819. end
  1820. end
  1821. end
  1822. end
  1823. def add_auto_watcher
  1824. if author&.active? &&
  1825. author&.allowed_to?(:add_issue_watchers, project) &&
  1826. author.pref.auto_watch_on?('issue_created') &&
  1827. self.watcher_user_ids.exclude?(author.id)
  1828. self.set_watcher(author, true)
  1829. end
  1830. end
  1831. def send_notification
  1832. if notify? && Setting.notified_events.include?('issue_added')
  1833. Mailer.deliver_issue_add(self)
  1834. end
  1835. end
  1836. def clear_disabled_fields
  1837. if tracker
  1838. tracker.disabled_core_fields.each do |attribute|
  1839. send :"#{attribute}=", nil
  1840. end
  1841. self.priority_id ||= IssuePriority.default&.id || IssuePriority.active.first.id
  1842. self.done_ratio ||= 0
  1843. end
  1844. end
  1845. def filter_projects_scope(scope=nil)
  1846. case scope
  1847. when 'system'
  1848. Project
  1849. when 'tree'
  1850. project.root.self_and_descendants
  1851. when 'hierarchy'
  1852. project.hierarchy
  1853. when 'descendants'
  1854. project.self_and_descendants
  1855. when ''
  1856. Project.where(:id => project.id)
  1857. else
  1858. Project
  1859. end
  1860. end
  1861. def roles_for_workflow(user)
  1862. roles = user.admin ? Role.all.to_a : user.roles_for_project(project)
  1863. roles.select(&:consider_workflow?)
  1864. end
  1865. end