BigW Consortium Gitlab

milestone.rb 4.17 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 committed
10 11
#  created_at  :datetime
#  updated_at  :datetime
Dmitriy Zaporozhets committed
12
#  state       :string(255)
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 :labels, through: :issues
31
  has_many :merge_requests
Andrey Kumanyaev committed
32
  has_many :participants, through: :issues, source: :assignee
33

34 35
  scope :active, -> { with_state(:active) }
  scope :closed, -> { with_state(:closed) }
36
  scope :of_projects, ->(ids) { where(project_id: ids) }
37

38
  validates :title, presence: true, uniqueness: { scope: :project_id }
Andrey Kumanyaev committed
39
  validates :project, presence: true
40

41 42
  strip_attributes :title

Andrew8xx8 committed
43
  state_machine :state, initial: :active do
44
    event :close do
Andrew8xx8 committed
45
      transition active: :closed
46 47 48
    end

    event :activate do
Andrew8xx8 committed
49
      transition closed: :active
50 51 52 53 54 55
    end

    state :closed

    state :active
  end
56

57 58
  alias_attribute :name, :title

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

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

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

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

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

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

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

87 88
  def expired?
    if due_date
89
      due_date.past?
90 91 92
    else
      false
    end
93
  end
94

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

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

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

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

113 114 115
  # Returns the elapsed time (in percent) since the Milestone creation date until today.
  # If the Milestone doesn't have a due_date then returns 0 since we can't calculate the elapsed time.
  # If the Milestone is overdue then it returns 100%.
Rubén Dávila committed
116
  def percent_time_used
Rubén Dávila committed
117
    return 0 unless due_date
Rubén Dávila committed
118 119 120 121 122 123 124 125
    return 100 if expired?

    duration = ((created_at - due_date.to_datetime) / 1.day)
    days_elapsed = ((created_at - Time.now) / 1.day)

    ((days_elapsed.to_f / duration) * 100).floor
  end

126
  def expires_at
127 128
    if due_date
      if due_date.past?
129
        "expired on #{due_date.to_s(:medium)}"
130
      else
131
        "expires on #{due_date.to_s(:medium)}"
132
      end
133
    end
134
  end
135 136

  def can_be_closed?
137
    active? && issues.opened.count.zero?
138 139 140 141
  end

  def is_empty?
    total_items_count.zero?
142 143
  end

144
  def author_id
145
    nil
146
  end
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178

  # 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
179
end