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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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, :case_sensitive => false
  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. if TYPES[relation_type]
  134. TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name]
  135. else
  136. :unknow
  137. end
  138. end
  139. def to_s(issue=nil)
  140. issue ||= issue_from
  141. issue_text = block_given? ? yield(other_issue(issue)) : "##{other_issue(issue).try(:id)}"
  142. s = []
  143. s << l(label_for(issue))
  144. s << "(#{l('datetime.distance_in_words.x_days', :count => delay)})" if delay && delay != 0
  145. s << issue_text
  146. s.join(' ')
  147. end
  148. def css_classes_for(issue)
  149. "rel-#{relation_type_for(issue)}"
  150. end
  151. def handle_issue_order
  152. reverse_if_needed
  153. if TYPE_PRECEDES == relation_type
  154. self.delay ||= 0
  155. else
  156. self.delay = nil
  157. end
  158. set_issue_to_dates
  159. end
  160. def set_issue_to_dates(journal=nil)
  161. soonest_start = self.successor_soonest_start
  162. if soonest_start && issue_to
  163. issue_to.reschedule_on!(soonest_start, journal)
  164. end
  165. end
  166. def successor_soonest_start
  167. if (TYPE_PRECEDES == self.relation_type) && delay && issue_from &&
  168. (issue_from.start_date || issue_from.due_date)
  169. add_working_days((issue_from.due_date || issue_from.start_date), (1 + delay))
  170. end
  171. end
  172. def <=>(relation)
  173. r = TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
  174. r == 0 ? id <=> relation.id : r
  175. end
  176. def init_journals(user)
  177. issue_from.init_journal(user) if issue_from
  178. issue_to.init_journal(user) if issue_to
  179. end
  180. private
  181. # Reverses the relation if needed so that it gets stored in the proper way
  182. # Should not be reversed before validation so that it can be displayed back
  183. # as entered on new relation form.
  184. #
  185. # Orders relates relations by ID, so that uniqueness index in DB is triggered
  186. # on concurrent access.
  187. def reverse_if_needed
  188. if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
  189. issue_tmp = issue_to
  190. self.issue_to = issue_from
  191. self.issue_from = issue_tmp
  192. self.relation_type = TYPES[relation_type][:reverse]
  193. elsif relation_type == TYPE_RELATES && issue_from_id > issue_to_id
  194. self.issue_to, self.issue_from = issue_from, issue_to
  195. end
  196. end
  197. # Returns true if the relation would create a circular dependency
  198. def circular_dependency?
  199. case relation_type
  200. when 'follows'
  201. issue_from.would_reschedule? issue_to
  202. when 'precedes'
  203. issue_to.would_reschedule? issue_from
  204. when 'blocked'
  205. issue_from.blocks? issue_to
  206. when 'blocks'
  207. issue_to.blocks? issue_from
  208. when 'relates'
  209. self.class.where(issue_from_id: issue_to, issue_to_id: issue_from).present?
  210. else
  211. false
  212. end
  213. end
  214. def call_issues_relation_added_callback
  215. call_issues_callback :relation_added
  216. end
  217. def call_issues_relation_removed_callback
  218. call_issues_callback :relation_removed
  219. end
  220. def call_issues_callback(name)
  221. [issue_from, issue_to].each do |issue|
  222. if issue
  223. issue.send name, self
  224. end
  225. end
  226. end
  227. end