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_relation.rb 8.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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 IssueRelation < ActiveRecord::Base
  19. # Class used to represent the relations of an issue
  20. class Relations < Array
  21. include Redmine::I18n
  22. def initialize(issue, *args)
  23. @issue = issue
  24. super(*args)
  25. end
  26. def to_s(*args)
  27. map {|relation| relation.to_s(@issue)}.join(', ')
  28. end
  29. end
  30. include Redmine::SafeAttributes
  31. include Redmine::Utils::DateCalculation
  32. belongs_to :issue_from, :class_name => 'Issue'
  33. belongs_to :issue_to, :class_name => 'Issue'
  34. TYPE_RELATES = "relates"
  35. TYPE_DUPLICATES = "duplicates"
  36. TYPE_DUPLICATED = "duplicated"
  37. TYPE_BLOCKS = "blocks"
  38. TYPE_BLOCKED = "blocked"
  39. TYPE_PRECEDES = "precedes"
  40. TYPE_FOLLOWS = "follows"
  41. TYPE_COPIED_TO = "copied_to"
  42. TYPE_COPIED_FROM = "copied_from"
  43. TYPES = {
  44. TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to,
  45. :order => 1, :sym => TYPE_RELATES },
  46. TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by,
  47. :order => 2, :sym => TYPE_DUPLICATED },
  48. TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates,
  49. :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
  50. TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by,
  51. :order => 4, :sym => TYPE_BLOCKED },
  52. TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks,
  53. :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS },
  54. TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows,
  55. :order => 6, :sym => TYPE_FOLLOWS },
  56. TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes,
  57. :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES },
  58. TYPE_COPIED_TO => { :name => :label_copied_to, :sym_name => :label_copied_from,
  59. :order => 8, :sym => TYPE_COPIED_FROM },
  60. TYPE_COPIED_FROM => { :name => :label_copied_from, :sym_name => :label_copied_to,
  61. :order => 9, :sym => TYPE_COPIED_TO, :reverse => TYPE_COPIED_TO }
  62. }.freeze
  63. validates_presence_of :issue_from, :issue_to, :relation_type
  64. validates_inclusion_of :relation_type, :in => TYPES.keys
  65. validates_numericality_of :delay, :allow_nil => true
  66. validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
  67. validate :validate_issue_relation
  68. before_save :handle_issue_order
  69. after_create :call_issues_relation_added_callback
  70. after_destroy :call_issues_relation_removed_callback
  71. safe_attributes 'relation_type',
  72. 'delay',
  73. 'issue_to_id'
  74. def safe_attributes=(attrs, user=User.current)
  75. if attrs.respond_to?(:to_unsafe_hash)
  76. attrs = attrs.to_unsafe_hash
  77. end
  78. return unless attrs.is_a?(Hash)
  79. attrs = attrs.deep_dup
  80. if issue_id = attrs.delete('issue_to_id')
  81. if issue_id.to_s.strip.match(/\A#?(\d+)\z/)
  82. issue_id = $1.to_i
  83. self.issue_to = Issue.visible(user).find_by_id(issue_id)
  84. end
  85. end
  86. super(attrs)
  87. end
  88. def visible?(user=User.current)
  89. (issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user))
  90. end
  91. def deletable?(user=User.current)
  92. visible?(user) &&
  93. ((issue_from.nil? || user.allowed_to?(:manage_issue_relations, issue_from.project)) ||
  94. (issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project)))
  95. end
  96. def initialize(attributes=nil, *args)
  97. super
  98. if new_record?
  99. if relation_type.blank?
  100. self.relation_type = IssueRelation::TYPE_RELATES
  101. end
  102. end
  103. end
  104. def validate_issue_relation
  105. if issue_from && issue_to
  106. errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
  107. unless issue_from.project_id == issue_to.project_id ||
  108. Setting.cross_project_issue_relations?
  109. errors.add :issue_to_id, :not_same_project
  110. end
  111. if circular_dependency?
  112. errors.add :base, :circular_dependency
  113. end
  114. if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
  115. errors.add :base, :cant_link_an_issue_with_a_descendant
  116. end
  117. end
  118. end
  119. def other_issue(issue)
  120. (self.issue_from_id == issue.id) ? issue_to : issue_from
  121. end
  122. # Returns the relation type for +issue+
  123. def relation_type_for(issue)
  124. if TYPES[relation_type]
  125. if self.issue_from_id == issue.id
  126. relation_type
  127. else
  128. TYPES[relation_type][:sym]
  129. end
  130. end
  131. end
  132. def label_for(issue)
  133. TYPES[relation_type] ?
  134. TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] :
  135. :unknow
  136. end
  137. def to_s(issue=nil)
  138. issue ||= issue_from
  139. issue_text = block_given? ? yield(other_issue(issue)) : "##{other_issue(issue).try(:id)}"
  140. s = []
  141. s << l(label_for(issue))
  142. s << "(#{l('datetime.distance_in_words.x_days', :count => delay)})" if delay && delay != 0
  143. s << issue_text
  144. s.join(' ')
  145. end
  146. def css_classes_for(issue)
  147. "rel-#{relation_type_for(issue)}"
  148. end
  149. def handle_issue_order
  150. reverse_if_needed
  151. if TYPE_PRECEDES == relation_type
  152. self.delay ||= 0
  153. else
  154. self.delay = nil
  155. end
  156. set_issue_to_dates
  157. end
  158. def set_issue_to_dates(journal=nil)
  159. soonest_start = self.successor_soonest_start
  160. if soonest_start && issue_to
  161. issue_to.reschedule_on!(soonest_start, journal)
  162. end
  163. end
  164. def successor_soonest_start
  165. if (TYPE_PRECEDES == self.relation_type) && delay && issue_from &&
  166. (issue_from.start_date || issue_from.due_date)
  167. add_working_days((issue_from.due_date || issue_from.start_date), (1 + delay))
  168. end
  169. end
  170. def <=>(relation)
  171. r = TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
  172. r == 0 ? id <=> relation.id : r
  173. end
  174. def init_journals(user)
  175. issue_from.init_journal(user) if issue_from
  176. issue_to.init_journal(user) if issue_to
  177. end
  178. private
  179. # Reverses the relation if needed so that it gets stored in the proper way
  180. # Should not be reversed before validation so that it can be displayed back
  181. # as entered on new relation form.
  182. #
  183. # Orders relates relations by ID, so that uniqueness index in DB is triggered
  184. # on concurrent access.
  185. def reverse_if_needed
  186. if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
  187. issue_tmp = issue_to
  188. self.issue_to = issue_from
  189. self.issue_from = issue_tmp
  190. self.relation_type = TYPES[relation_type][:reverse]
  191. elsif relation_type == TYPE_RELATES && issue_from_id > issue_to_id
  192. self.issue_to, self.issue_from = issue_from, issue_to
  193. end
  194. end
  195. # Returns true if the relation would create a circular dependency
  196. def circular_dependency?
  197. case relation_type
  198. when 'follows'
  199. issue_from.would_reschedule? issue_to
  200. when 'precedes'
  201. issue_to.would_reschedule? issue_from
  202. when 'blocked'
  203. issue_from.blocks? issue_to
  204. when 'blocks'
  205. issue_to.blocks? issue_from
  206. when 'relates'
  207. self.class.where(issue_from_id: issue_to, issue_to_id: issue_from).present?
  208. else
  209. false
  210. end
  211. end
  212. def call_issues_relation_added_callback
  213. call_issues_callback :relation_added
  214. end
  215. def call_issues_relation_removed_callback
  216. call_issues_callback :relation_removed
  217. end
  218. def call_issues_callback(name)
  219. [issue_from, issue_to].each do |issue|
  220. if issue
  221. issue.send name, self
  222. end
  223. end
  224. end
  225. end