event.rb 7.74 KB
Newer Older
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1
class Event < ActiveRecord::Base
2
  include Sortable
3
  include IgnorableColumn
4
  default_scope { reorder(nil).where.not(author_id: nil) }
5

6 7 8 9 10 11 12 13 14
  CREATED   = 1
  UPDATED   = 2
  CLOSED    = 3
  REOPENED  = 4
  PUSHED    = 5
  COMMENTED = 6
  MERGED    = 7
  JOINED    = 8 # User joined project
  LEFT      = 9 # User left project
15
  DESTROYED = 10
16
  EXPIRED   = 11 # User left project due to expiry
17

Mark Fletcher's avatar
Mark Fletcher committed
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
  ACTIONS = HashWithIndifferentAccess.new(
    created:    CREATED,
    updated:    UPDATED,
    closed:     CLOSED,
    reopened:   REOPENED,
    pushed:     PUSHED,
    commented:  COMMENTED,
    merged:     MERGED,
    joined:     JOINED,
    left:       LEFT,
    destroyed:  DESTROYED,
    expired:    EXPIRED
  ).freeze

  TARGET_TYPES = HashWithIndifferentAccess.new(
    issue:          Issue,
    milestone:      Milestone,
    merge_request:  MergeRequest,
    note:           Note,
    project:        Project,
    snippet:        Snippet,
    user:           User
  ).freeze

42 43
  RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour

44
  delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
Nihad Abbasov's avatar
Nihad Abbasov committed
45 46
  delegate :title, to: :issue, prefix: true, allow_nil: true
  delegate :title, to: :merge_request, prefix: true, allow_nil: true
47
  delegate :title, to: :note, prefix: true, allow_nil: true
Nihad Abbasov's avatar
Nihad Abbasov committed
48

randx's avatar
randx committed
49
  belongs_to :author, class_name: "User"
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
50
  belongs_to :project
51
  belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
52
  has_one :push_event_payload
53

54 55
  # Callbacks
  after_create :reset_project_activity
56
  after_create :set_last_repository_updated_at, if: :push?
57

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
58
  # Scopes
59
  scope :recent, -> { reorder(id: :desc) }
60
  scope :code_push, -> { where(action: PUSHED) }
61

62 63 64 65 66 67 68
  scope :in_projects, -> (projects) do
    sub_query = projects
      .except(:order)
      .select(1)
      .where('projects.id = events.project_id')

    where('EXISTS (?)', sub_query).recent
69 70
  end

71 72 73 74 75 76 77
  scope :with_associations, -> do
    # We're using preload for "push_event_payload" as otherwise the association
    # is not always available (depending on the query being built).
    includes(:author, :project, project: :namespace)
      .preload(:target, :push_event_payload)
  end

78
  scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
79

80 81
  self.inheritance_column = 'action'

82 83 84 85
  # "data" will be removed in 10.0 but it may be possible that JOINs happen that
  # include this column, hence we're ignoring it as well.
  ignore_column :data

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
86
  class << self
87 88 89 90
    def model_name
      ActiveModel::Name.new(self, nil, 'event')
    end

91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
    def find_sti_class(action)
      if action.to_i == PUSHED
        PushEvent
      else
        Event
      end
    end

    def subclass_from_attributes(attrs)
      # Without this Rails will keep calling this method on the returned class,
      # resulting in an infinite loop.
      return unless self == Event

      action = attrs.with_indifferent_access[inheritance_column].to_i

      PushEvent if action == PUSHED
    end

109
    # Update Gitlab::ContributionsCalendar#activity_dates if this changes
110
    def contributions
111 112
      where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)",
            Event::PUSHED,
Douwe Maan's avatar
Douwe Maan committed
113
            %w(MergeRequest Issue), [Event::CREATED, Event::CLOSED, Event::MERGED],
114
            "Note", Event::COMMENTED)
115
    end
116

Yorick Peterse's avatar
Yorick Peterse committed
117 118 119
    def limit_recent(limit = 20, offset = nil)
      recent.limit(limit).offset(offset)
    end
Mark Fletcher's avatar
Mark Fletcher committed
120 121 122 123 124 125 126 127

    def actions
      ACTIONS.keys
    end

    def target_types
      TARGET_TYPES.keys
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
128 129
  end

130
  def visible_to_user?(user = nil)
131
    if push? || commit_note?
132
      Ability.allowed?(user, :download_code, project)
133 134
    elsif membership_changed?
      true
135 136
    elsif created_project?
      true
137
    elsif issue? || issue_note?
138
      Ability.allowed?(user, :read_issue, note? ? note_target : target)
139 140
    elsif merge_request? || merge_request_note?
      Ability.allowed?(user, :read_merge_request, note? ? note_target : target)
141
    else
142
      milestone?
143
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
144 145
  end

146 147
  def project_name
    if project
148
      project.name_with_namespace
149
    else
Riyad Preukschas's avatar
Riyad Preukschas committed
150
      "(deleted project)"
151 152 153
    end
  end

154
  def target_title
155
    target.try(:title)
156 157 158 159
  end

  def created?
    action == CREATED
160 161
  end

162
  def push?
163
    false
164 165
  end

166
  def merged?
167
    action == MERGED
168 169
  end

170
  def closed?
171
    action == CLOSED
172 173 174
  end

  def reopened?
175 176 177 178 179 180 181 182 183 184 185
    action == REOPENED
  end

  def joined?
    action == JOINED
  end

  def left?
    action == LEFT
  end

186 187 188 189
  def expired?
    action == EXPIRED
  end

190 191 192 193
  def destroyed?
    action == DESTROYED
  end

194 195 196 197 198
  def commented?
    action == COMMENTED
  end

  def membership_changed?
199
    joined? || left? || expired?
200 201
  end

202
  def created_project?
203
    created? && !target && target_type.nil?
204 205 206 207 208 209
  end

  def created_target?
    created? && target
  end

210 211 212 213 214
  def milestone?
    target_type == "Milestone"
  end

  def note?
215
    target.is_a?(Note)
216 217
  end

218
  def issue?
219
    target_type == "Issue"
220 221
  end

222
  def merge_request?
223
    target_type == "MergeRequest"
224 225
  end

226 227
  def milestone
    target if milestone?
228 229
  end

230
  def issue
231
    target if issue?
232 233 234
  end

  def merge_request
235
    target if merge_request?
236 237
  end

238
  def note
239
    target if note?
240 241
  end

242
  def action_name
243 244 245 246 247 248 249 250 251
    if push?
      if new_ref?
        "pushed new"
      elsif rm_ref?
        "deleted"
      else
        "pushed to"
      end
    elsif closed?
252 253
      "closed"
    elsif merged?
254
      "accepted"
255 256
    elsif joined?
      'joined'
Alex Denisov's avatar
Alex Denisov committed
257 258
    elsif left?
      'left'
259 260
    elsif expired?
      'removed due to membership expiration from'
261 262
    elsif destroyed?
      'destroyed'
263 264
    elsif commented?
      "commented on"
265
    elsif created_project?
266
      if project.external_import?
267 268 269 270
        "imported"
      else
        "created"
      end
271
    else
272
      "opened"
273 274
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
275

276 277 278 279
  def target_iid
    target.respond_to?(:iid) ? target.iid : target_id
  end

280
  def commit_note?
281
    note? && target && target.for_commit?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
282 283
  end

284
  def issue_note?
285
    note? && target && target.for_issue?
286 287
  end

288 289 290 291
  def merge_request_note?
    note? && target && target.for_merge_request?
  end

292
  def project_snippet_note?
293
    note? && target && target.for_snippet?
Andrew8xx8's avatar
Andrew8xx8 committed
294 295
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
296 297 298 299 300
  def note_target
    target.noteable
  end

  def note_target_id
301
    if commit_note?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
302 303 304 305 306 307
      target.commit_id
    else
      target.noteable_id.to_s
    end
  end

308 309 310 311 312 313
  def note_target_reference
    return unless note_target

    # Commit#to_reference returns the full SHA, but we want the short one here
    if commit_note?
      note_target.short_id
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
314
    else
315 316
      note_target.to_reference
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
317 318
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
319 320 321 322 323 324 325
  def note_target_type
    if target.noteable_type.present?
      target.noteable_type.titleize
    else
      "Wall"
    end.downcase
  end
326 327 328

  def body?
    if push?
329
      push_with_commits?
330 331 332 333 334 335
    elsif note?
      true
    else
      target.respond_to? :title
    end
  end
336 337

  def reset_project_activity
338 339
    return unless project

340
    # Don't bother updating if we know the project was updated recently.
341
    return if recent_update?
342

343 344 345
    # At this point it's possible for multiple threads/processes to try to
    # update the project. Only one query should actually perform the update,
    # hence we add the extra WHERE clause for last_activity_at.
346 347 348
    Project.unscoped.where(id: project_id)
      .where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago)
      .update_all(last_activity_at: created_at)
349 350
  end

351 352 353 354
  def authored_by?(user)
    user ? author_id == user.id : false
  end

355 356 357 358 359 360
  def to_partial_path
    # We are intentionally using `Event` rather than `self.class` so that
    # subclasses also use the `Event` implementation.
    Event._to_partial_path
  end

361 362 363 364 365
  private

  def recent_update?
    project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
  end
366 367

  def set_last_repository_updated_at
368 369
    Project.unscoped.where(id: project_id)
      .update_all(last_repository_updated_at: created_at)
370
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
371
end