issuable.rb 4.6 KB
Newer Older
1
# == Issuable concern
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
2
#
3
# Contains common functionality shared between Issues and MergeRequests
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
4
5
6
#
# Used by Issue, MergeRequest
#
7
module Issuable
8
  extend ActiveSupport::Concern
9
  include Participable
10
  include Mentionable
11
12

  included do
13
14
    belongs_to :author, class_name: "User"
    belongs_to :assignee, class_name: "User"
15
    belongs_to :updated_by, class_name: "User"
16
    belongs_to :milestone
17
    has_many :notes, as: :noteable, dependent: :destroy
18
19
    has_many :label_links, as: :target, dependent: :destroy
    has_many :labels, through: :label_links
20
    has_many :subscriptions, dependent: :destroy, as: :subscribable
21

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
22
23
    validates :author, presence: true
    validates :title, presence: true, length: { within: 0..255 }
24

25
    scope :authored, ->(user) { where(author_id: user) }
26
    scope :assigned_to, ->(u) { where(assignee_id: u.id)}
Andrew8xx8's avatar
Andrew8xx8 committed
27
    scope :recent, -> { order("created_at DESC") }
28
29
    scope :assigned, -> { where("assignee_id IS NOT NULL") }
    scope :unassigned, -> { where("assignee_id IS NULL") }
30
    scope :of_projects, ->(ids) { where(project_id: ids) }
31
    scope :opened, -> { with_state(:opened, :reopened) }
32
33
    scope :only_opened, -> { with_state(:opened) }
    scope :only_reopened, -> { with_state(:reopened) }
34
    scope :closed, -> { with_state(:closed) }
35
36
    scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') }
    scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') }
37

38
39
    delegate :name,
             :email,
40
41
             to: :author,
             prefix: true
42
43
44

    delegate :name,
             :email,
45
46
47
             to: :assignee,
             allow_nil: true,
             prefix: true
48

49
50
    attr_mentionable :title 
    attr_mentionable :description, cache: true
51
    participant :author, :assignee, :notes
52
53
  end

54
55
  module ClassMethods
    def search(query)
56
      where("LOWER(title) like :query", query: "%#{query.downcase}%")
57
    end
58

59
60
61
62
    def full_search(query)
      where("LOWER(title) like :query OR LOWER(description) like :query", query: "%#{query.downcase}%")
    end

63
    def sort(method)
64
65
66
67
68
69
      case method.to_s
      when 'milestone_due_asc' then order_milestone_due_asc
      when 'milestone_due_desc' then order_milestone_due_desc
      else
        order_by(method)
      end
70
    end
71
72
73
74
75
76
77
78
79
  end

  def today?
    Date.today == created_at.to_date
  end

  def new?
    today? && created_at == updated_at
  end
80
81
82
83
84
85
86
87
88

  def is_assigned?
    !!assignee_id
  end

  def is_being_reassigned?
    assignee_id_changed?
  end

89
90
91
92
93
94
  #
  # Votes
  #

  # Return the number of -1 comments (downvotes)
  def downvotes
95
    filter_superceded_votes(notes.select(&:downvote?), notes).size
96
97
  end

98
  def downvotes_in_percent
99
100
101
    if votes_count.zero?
      0
    else
102
      100.0 - upvotes_in_percent
103
104
105
    end
  end

106
107
  # Return the number of +1 comments (upvotes)
  def upvotes
108
    filter_superceded_votes(notes.select(&:upvote?), notes).size
109
110
  end

111
  def upvotes_in_percent
112
113
114
    if votes_count.zero?
      0
    else
115
      100.0 / votes_count * upvotes
116
117
118
119
120
121
122
    end
  end

  # Return the total number of votes
  def votes_count
    upvotes + downvotes
  end
123

Valery Sizov's avatar
tests    
Valery Sizov committed
124
  def subscribed?(user)
125
126
127
128
    subscription = subscriptions.find_by_user_id(user.id)

    if subscription
      return subscription.subscribed
Valery Sizov's avatar
Valery Sizov committed
129
130
    end

131
    participants(user).include?(user)
Valery Sizov's avatar
Valery Sizov committed
132
133
  end

Valery Sizov's avatar
tests    
Valery Sizov committed
134
135
136
137
138
139
  def toggle_subscription(user)
    subscriptions.
      find_or_initialize_by(user_id: user.id).
      update(subscribed: !subscribed?(user))
  end

Kirill Zaitsev's avatar
Kirill Zaitsev committed
140
  def to_hook_data(user)
141
142
    {
      object_kind: self.class.name.underscore,
Kirill Zaitsev's avatar
Kirill Zaitsev committed
143
      user: user.hook_attrs,
144
145
146
147
148
149
      repository: {
          name: project.name,
          url: project.url_to_repo,
          description: project.description,
          homepage: project.web_url
      },
Kirill Zaitsev's avatar
Kirill Zaitsev committed
150
      object_attributes: hook_attrs
151
152
    }
  end
153
154
155
156
157

  def label_names
    labels.order('title ASC').pluck(:title)
  end

158
159
160
161
  def remove_labels
    labels.delete_all
  end

162
163
  def add_labels_by_names(label_names)
    label_names.each do |label_name|
164
165
      label = project.labels.create_with(color: Label::DEFAULT_COLOR).
        find_or_create_by(title: label_name.strip)
166
167
168
      self.labels << label
    end
  end
169

Robert Speicher's avatar
Robert Speicher committed
170
171
172
173
174
175
176
177
178
179
  # Convert this Issuable class name to a format usable by Ability definitions
  #
  # Examples:
  #
  #   issuable.class           # => MergeRequest
  #   issuable.to_ability_name # => "merge_request"
  def to_ability_name
    self.class.to_s.underscore
  end

180
181
182
183
  private

  def filter_superceded_votes(votes, notes)
    filteredvotes = [] + votes
184

185
186
187
188
189
    votes.each do |vote|
      if vote.superceded?(notes)
        filteredvotes.delete(vote)
      end
    end
190

191
192
    filteredvotes
  end
193
end