PageRenderTime 39ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/app/models/milestone.rb

https://gitlab.com/emrox/gitlab-ce
Ruby | 221 lines | 143 code | 35 blank | 43 comment | 8 complexity | f4267eff24c900be7fb879af32a9a1e5 MD5 | raw file
  1. class Milestone < ActiveRecord::Base
  2. # Represents a "No Milestone" state used for filtering Issues and Merge
  3. # Requests that have no milestone assigned.
  4. MilestoneStruct = Struct.new(:title, :name, :id)
  5. None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
  6. Any = MilestoneStruct.new('Any Milestone', '', -1)
  7. Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
  8. Started = MilestoneStruct.new('Started', '#started', -3)
  9. include CacheMarkdownField
  10. include InternalId
  11. include Sortable
  12. include Referable
  13. include StripAttribute
  14. include Milestoneish
  15. cache_markdown_field :title, pipeline: :single_line
  16. cache_markdown_field :description
  17. belongs_to :project
  18. has_many :issues
  19. has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
  20. has_many :merge_requests
  21. has_many :participants, -> { distinct.reorder('users.name') }, through: :issues, source: :assignee
  22. has_many :events, as: :target, dependent: :destroy
  23. scope :active, -> { with_state(:active) }
  24. scope :closed, -> { with_state(:closed) }
  25. scope :of_projects, ->(ids) { where(project_id: ids) }
  26. validates :title, presence: true, uniqueness: { scope: :project_id }
  27. validates :project, presence: true
  28. validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
  29. strip_attributes :title
  30. state_machine :state, initial: :active do
  31. event :close do
  32. transition active: :closed
  33. end
  34. event :activate do
  35. transition closed: :active
  36. end
  37. state :closed
  38. state :active
  39. end
  40. alias_attribute :name, :title
  41. class << self
  42. # Searches for milestones matching the given query.
  43. #
  44. # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
  45. #
  46. # query - The search query as a String
  47. #
  48. # Returns an ActiveRecord::Relation.
  49. def search(query)
  50. t = arel_table
  51. pattern = "%#{query}%"
  52. where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
  53. end
  54. end
  55. def self.reference_prefix
  56. '%'
  57. end
  58. def self.reference_pattern
  59. # NOTE: The iid pattern only matches when all characters on the expression
  60. # are digits, so it will match %2 but not %2.1 because that's probably a
  61. # milestone name and we want it to be matched as such.
  62. @reference_pattern ||= %r{
  63. (#{Project.reference_pattern})?
  64. #{Regexp.escape(reference_prefix)}
  65. (?:
  66. (?<milestone_iid>
  67. \d+(?!\S\w)\b # Integer-based milestone iid, or
  68. ) |
  69. (?<milestone_name>
  70. [^"\s]+\b | # String-based single-word milestone title, or
  71. "[^"]+" # String-based multi-word milestone surrounded in quotes
  72. )
  73. )
  74. }x
  75. end
  76. def self.link_reference_pattern
  77. @link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/)
  78. end
  79. def self.upcoming_ids_by_projects(projects)
  80. rel = unscoped.of_projects(projects).active.where('due_date > ?', Time.now)
  81. if Gitlab::Database.postgresql?
  82. rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id')
  83. else
  84. rel.
  85. group(:project_id).
  86. having('due_date = MIN(due_date)').
  87. pluck(:id, :project_id, :due_date).
  88. map(&:first)
  89. end
  90. end
  91. def self.sort(method)
  92. case method.to_s
  93. when 'due_date_asc'
  94. reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC'))
  95. when 'due_date_desc'
  96. reorder(Gitlab::Database.nulls_last_order('due_date', 'DESC'))
  97. when 'start_date_asc'
  98. reorder(Gitlab::Database.nulls_last_order('start_date', 'ASC'))
  99. when 'start_date_desc'
  100. reorder(Gitlab::Database.nulls_last_order('start_date', 'DESC'))
  101. else
  102. order_by(method)
  103. end
  104. end
  105. ##
  106. # Returns the String necessary to reference this Milestone in Markdown
  107. #
  108. # format - Symbol format to use (default: :iid, optional: :name)
  109. #
  110. # Examples:
  111. #
  112. # Milestone.first.to_reference # => "%1"
  113. # Milestone.first.to_reference(format: :name) # => "%\"goal\""
  114. # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1"
  115. # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
  116. #
  117. def to_reference(from_project = nil, format: :iid, full: false)
  118. format_reference = milestone_format_reference(format)
  119. reference = "#{self.class.reference_prefix}#{format_reference}"
  120. "#{project.to_reference(from_project, full: full)}#{reference}"
  121. end
  122. def reference_link_text(from_project = nil)
  123. self.title
  124. end
  125. def milestoneish_ids
  126. id
  127. end
  128. def can_be_closed?
  129. active? && issues.opened.count.zero?
  130. end
  131. def author_id
  132. nil
  133. end
  134. def title=(value)
  135. write_attribute(:title, sanitize_title(value)) if value.present?
  136. end
  137. # Sorts the issues for the given IDs.
  138. #
  139. # This method runs a single SQL query using a CASE statement to update the
  140. # position of all issues in the current milestone (scoped to the list of IDs).
  141. #
  142. # Given the ids [10, 20, 30] this method produces a SQL query something like
  143. # the following:
  144. #
  145. # UPDATE issues
  146. # SET position = CASE
  147. # WHEN id = 10 THEN 1
  148. # WHEN id = 20 THEN 2
  149. # WHEN id = 30 THEN 3
  150. # ELSE position
  151. # END
  152. # WHERE id IN (10, 20, 30);
  153. #
  154. # This method expects that the IDs given in `ids` are already Fixnums.
  155. def sort_issues(ids)
  156. pairs = []
  157. ids.each_with_index do |id, index|
  158. pairs << id
  159. pairs << index + 1
  160. end
  161. conditions = 'WHEN id = ? THEN ? ' * ids.length
  162. issues.where(id: ids).
  163. update_all(["position = CASE #{conditions} ELSE position END", *pairs])
  164. end
  165. private
  166. def milestone_format_reference(format = :iid)
  167. raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format)
  168. if format == :name && !name.include?('"')
  169. %("#{name}")
  170. else
  171. iid
  172. end
  173. end
  174. def sanitize_title(value)
  175. CGI.unescape_html(Sanitize.clean(value.to_s))
  176. end
  177. def start_date_should_be_less_than_due_date
  178. if due_date <= start_date
  179. errors.add(:start_date, "Can't be greater than due date")
  180. end
  181. end
  182. def issues_finder_params
  183. { project_id: project_id }
  184. end
  185. end