milestone.rb 3.59 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 74 75 76 77 78 79 80 81
  def self.reference_pattern
    nil
  end

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

  def to_reference(from_project = nil)
    h = Gitlab::Application.routes.url_helpers
    h.namespace_project_milestone_url(self.project.namespace, self.project, self)
  end

  def reference_link_text(from_project = nil)
    %Q{<i class="fa fa-clock-o"></i> }.html_safe + self.title
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
82 83
  def expired?
    if due_date
84
      due_date.past?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
85 86 87
    else
      false
    end
88
  end
89

90 91
  def open_items_count
    self.issues.opened.count + self.merge_requests.opened.count
92 93
  end

94
  def closed_items_count
95
    self.issues.closed.count + self.merge_requests.closed_and_merged.count
96 97 98 99
  end

  def total_items_count
    self.issues.count + self.merge_requests.count
100 101 102
  end

  def percent_complete
103
    ((closed_items_count * 100) / total_items_count).abs
104
  rescue ZeroDivisionError
105
    0
106 107 108
  end

  def expires_at
109 110 111 112
    if due_date
      if due_date.past?
        "expired at #{due_date.stamp("Aug 21, 2011")}"
      else
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
113
        "expires at #{due_date.stamp("Aug 21, 2011")}"
114
      end
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
115
    end
116
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
117 118

  def can_be_closed?
Andrew8xx8's avatar
Andrew8xx8 committed
119
    active? && issues.opened.count.zero?
120 121 122 123
  end

  def is_empty?
    total_items_count.zero?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
124 125
  end

126
  def author_id
127
    nil
128
  end
129 130 131 132 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

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