milestone.rb 3.62 KB
Newer Older
1 2 3 4 5 6 7 8 9
# == Schema Information
#
# Table name: milestones
#
#  id          :integer          not null, primary key
#  title       :string(255)      not null
#  project_id  :integer          not null
#  description :text
#  due_date    :date
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
10 11
#  created_at  :datetime
#  updated_at  :datetime
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
12
#  state       :string(255)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
13
#  iid         :integer
14 15
#

16
class Milestone < ActiveRecord::Base
17 18
  # Represents a "No Milestone" state used for filtering Issues and Merge
  # Requests that have no milestone assigned.
19 20 21
  MilestoneStruct = Struct.new(:title, :name, :id)
  None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
  Any = MilestoneStruct.new('Any Milestone', '', -1)
22

23
  include InternalId
24
  include Sortable
25
  include Referable
26
  include StripAttribute
27

28 29
  belongs_to :project
  has_many :issues
30
  has_many :merge_requests
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
31
  has_many :participants, through: :issues, source: :assignee
32

Andrew8xx8's avatar
Andrew8xx8 committed
33 34
  scope :active, -> { with_state(:active) }
  scope :closed, -> { with_state(:closed) }
35
  scope :of_projects, ->(ids) { where(project_id: ids) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
36

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
37 38
  validates :title, presence: true
  validates :project, presence: true
Andrew8xx8's avatar
Andrew8xx8 committed
39

40 41
  strip_attributes :title

Andrew8xx8's avatar
Andrew8xx8 committed
42
  state_machine :state, initial: :active do
Andrew8xx8's avatar
Andrew8xx8 committed
43
    event :close do
Andrew8xx8's avatar
Andrew8xx8 committed
44
      transition active: :closed
Andrew8xx8's avatar
Andrew8xx8 committed
45 46 47
    end

    event :activate do
Andrew8xx8's avatar
Andrew8xx8 committed
48
      transition closed: :active
Andrew8xx8's avatar
Andrew8xx8 committed
49 50 51 52 53 54
    end

    state :closed

    state :active
  end
55

56 57
  alias_attribute :name, :title

58 59 60 61 62 63 64
  class << self
    def search(query)
      query = "%#{query}%"
      where("title like ? or description like ?", query, query)
    end
  end

65 66 67 68 69 70 71 72 73
  def self.reference_pattern
    nil
  end

  def self.link_reference_pattern
    super("milestones", /(?<milestone>\d+)/)
  end

  def to_reference(from_project = nil)
74 75
    escaped_title = self.title.gsub("]", "\\]")

76
    h = Gitlab::Application.routes.url_helpers
77 78 79
    url = h.namespace_project_milestone_url(self.project.namespace, self.project, self)

    "[#{escaped_title}](#{url})"
80 81 82
  end

  def reference_link_text(from_project = nil)
83
    self.title
84 85
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
86 87
  def expired?
    if due_date
88
      due_date.past?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
89 90 91
    else
      false
    end
92
  end
93

94 95
  def open_items_count
    self.issues.opened.count + self.merge_requests.opened.count
96 97
  end

98
  def closed_items_count
99
    self.issues.closed.count + self.merge_requests.closed_and_merged.count
100 101 102 103
  end

  def total_items_count
    self.issues.count + self.merge_requests.count
104 105 106
  end

  def percent_complete
107
    ((closed_items_count * 100) / total_items_count).abs
108
  rescue ZeroDivisionError
109
    0
110 111 112
  end

  def expires_at
113 114
    if due_date
      if due_date.past?
115
        "expired on #{due_date.to_s(:medium)}"
116
      else
117
        "expires on #{due_date.to_s(:medium)}"
118
      end
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
119
    end
120
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
121 122

  def can_be_closed?
Andrew8xx8's avatar
Andrew8xx8 committed
123
    active? && issues.opened.count.zero?
124 125 126 127
  end

  def is_empty?
    total_items_count.zero?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
128 129
  end

130
  def author_id
131
    nil
132
  end
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164

  # Sorts the issues for the given IDs.
  #
  # This method runs a single SQL query using a CASE statement to update the
  # position of all issues in the current milestone (scoped to the list of IDs).
  #
  # Given the ids [10, 20, 30] this method produces a SQL query something like
  # the following:
  #
  #     UPDATE issues
  #     SET position = CASE
  #       WHEN id = 10 THEN 1
  #       WHEN id = 20 THEN 2
  #       WHEN id = 30 THEN 3
  #       ELSE position
  #     END
  #     WHERE id IN (10, 20, 30);
  #
  # This method expects that the IDs given in `ids` are already Fixnums.
  def sort_issues(ids)
    pairs = []

    ids.each_with_index do |id, index|
      pairs << id
      pairs << index + 1
    end

    conditions = 'WHEN id = ? THEN ? ' * ids.length

    issues.where(id: ids).
      update_all(["position = CASE #{conditions} ELSE position END", *pairs])
  end
165
end