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 65KB

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