milestone.rb 3.09 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)
  None = MilestoneStruct.new('No Milestone', 'No Milestone')
  Any = MilestoneStruct.new('Any', '')
22

23
  include InternalId
24
  include Sortable
25

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

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

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
35 36
  validates :title, presence: true
  validates :project, presence: true
Andrew8xx8's avatar
Andrew8xx8 committed
37

Andrew8xx8's avatar
Andrew8xx8 committed
38
  state_machine :state, initial: :active do
Andrew8xx8's avatar
Andrew8xx8 committed
39
    event :close do
Andrew8xx8's avatar
Andrew8xx8 committed
40
      transition active: :closed
Andrew8xx8's avatar
Andrew8xx8 committed
41 42 43
    end

    event :activate do
Andrew8xx8's avatar
Andrew8xx8 committed
44
      transition closed: :active
Andrew8xx8's avatar
Andrew8xx8 committed
45 46 47 48 49 50
    end

    state :closed

    state :active
  end
51

52 53
  alias_attribute :name, :title

54 55 56 57 58 59 60
  class << self
    def search(query)
      query = "%#{query}%"
      where("title like ? or description like ?", query, query)
    end
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
61 62
  def expired?
    if due_date
63
      due_date.past?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
64 65 66
    else
      false
    end
67
  end
68

69 70
  def open_items_count
    self.issues.opened.count + self.merge_requests.opened.count
71 72
  end

73
  def closed_items_count
74
    self.issues.closed.count + self.merge_requests.closed_and_merged.count
75 76 77 78
  end

  def total_items_count
    self.issues.count + self.merge_requests.count
79 80 81
  end

  def percent_complete
82
    ((closed_items_count * 100) / total_items_count).abs
83
  rescue ZeroDivisionError
84
    0
85 86 87
  end

  def expires_at
88 89 90 91
    if due_date
      if due_date.past?
        "expired at #{due_date.stamp("Aug 21, 2011")}"
      else
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
92
        "expires at #{due_date.stamp("Aug 21, 2011")}"
93
      end
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
94
    end
95
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
96 97

  def can_be_closed?
Andrew8xx8's avatar
Andrew8xx8 committed
98
    active? && issues.opened.count.zero?
99 100 101 102
  end

  def is_empty?
    total_items_count.zero?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
103 104
  end

105
  def author_id
106
    nil
107
  end
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139

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