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.2KB

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