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

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