Read about our upcoming Code of Conduct on this issue

This instance was upgraded to Heptapod 0.28.0 today

event.rb 9.11 KB
Newer Older
1
2
# frozen_string_literal: true

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
3
class Event < ActiveRecord::Base
4
  include Sortable
5
  include IgnorableColumn
6
  include FromUnion
7
  default_scope { reorder(nil) }
8

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

Mark Fletcher's avatar
Mark Fletcher committed
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
  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

45
  RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
46
  REPOSITORY_UPDATED_AT_INTERVAL = 5.minutes
47

48
  delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
Nihad Abbasov's avatar
Nihad Abbasov committed
49
50
  delegate :title, to: :issue, prefix: true, allow_nil: true
  delegate :title, to: :merge_request, prefix: true, allow_nil: true
51
  delegate :title, to: :note, prefix: true, allow_nil: true
Nihad Abbasov's avatar
Nihad Abbasov committed
52

randx's avatar
randx committed
53
  belongs_to :author, class_name: "User"
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
54
  belongs_to :project
55
56
57
58

  belongs_to :target, -> {
    # If the association for "target" defines an "author" association we want to
    # eager-load this so Banzai & friends don't end up performing N+1 queries to
59
60
61
    # get the authors of notes, issues, etc. (likewise for "noteable").
    incs = %i(author noteable).select do |a|
      reflections['events'].active_record.reflect_on_association(a)
62
    end
63
64

    incs.reduce(self) { |obj, a| obj.includes(a) }
65
66
  }, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations

67
  has_one :push_event_payload
68

69
70
  # Callbacks
  after_create :reset_project_activity
71
  after_create :set_last_repository_updated_at, if: :push?
72
  after_create :track_user_interacted_projects
73

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
74
  # Scopes
75
  scope :recent, -> { reorder(id: :desc) }
76
  scope :code_push, -> { where(action: PUSHED) }
77

78
79
80
81
82
83
84
  scope :in_projects, -> (projects) do
    sub_query = projects
      .except(:order)
      .select(1)
      .where('projects.id = events.project_id')

    where('EXISTS (?)', sub_query).recent
85
86
  end

87
88
89
90
  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)
91
      .preload(:target, :push_event_payload)
92
93
  end

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

96
97
98
99
100
101
  # Authors are required as they're used to display who pushed data.
  #
  # We're just validating the presence of the ID here as foreign key constraints
  # should ensure the ID points to a valid user.
  validates :author_id, presence: true

102
103
  self.inheritance_column = 'action'

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
104
  class << self
105
106
107
108
    def model_name
      ActiveModel::Name.new(self, nil, 'event')
    end

109
110
111
112
113
114
115
116
    def find_sti_class(action)
      if action.to_i == PUSHED
        PushEvent
      else
        Event
      end
    end

117
    # Remove this method when removing Gitlab.rails5? code.
118
    def subclass_from_attributes(attrs)
119
120
      return super if Gitlab.rails5?

121
122
123
124
125
126
127
128
129
      # 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

130
    # Update Gitlab::ContributionsCalendar#activity_dates if this changes
131
    def contributions
132
133
      where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)",
            Event::PUSHED,
Douwe Maan's avatar
Douwe Maan committed
134
            %w(MergeRequest Issue), [Event::CREATED, Event::CLOSED, Event::MERGED],
135
            "Note", Event::COMMENTED)
136
    end
137

Yorick Peterse's avatar
Yorick Peterse committed
138
139
140
    def limit_recent(limit = 20, offset = nil)
      recent.limit(limit).offset(offset)
    end
Mark Fletcher's avatar
Mark Fletcher committed
141
142
143
144
145
146
147
148

    def actions
      ACTIONS.keys
    end

    def target_types
      TARGET_TYPES.keys
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
149
150
  end

151
  def visible_to_user?(user = nil)
152
    if push? || commit_note?
153
      Ability.allowed?(user, :download_code, project)
154
    elsif membership_changed?
155
      Ability.allowed?(user, :read_project, project)
156
    elsif created_project?
157
      Ability.allowed?(user, :read_project, project)
158
    elsif issue? || issue_note?
http://jneen.net/'s avatar
http://jneen.net/ committed
159
      Ability.allowed?(user, :read_issue, note? ? note_target : target)
Valery Sizov's avatar
Valery Sizov committed
160
161
    elsif merge_request? || merge_request_note?
      Ability.allowed?(user, :read_merge_request, note? ? note_target : target)
162
163
    elsif milestone?
      Ability.allowed?(user, :read_project, project)
164
    else
165
      false # No other event types are visible
166
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
167
168
  end

169
170
  def project_name
    if project
171
      project.full_name
172
    else
Riyad Preukschas's avatar
Riyad Preukschas committed
173
      "(deleted project)"
174
175
176
    end
  end

177
  def target_title
178
    target.try(:title)
179
180
181
182
  end

  def created?
    action == CREATED
183
184
  end

185
  def push?
186
    false
187
188
  end

189
  def merged?
190
    action == MERGED
191
192
  end

193
  def closed?
194
    action == CLOSED
195
196
197
  end

  def reopened?
198
199
200
201
202
203
204
205
206
207
208
    action == REOPENED
  end

  def joined?
    action == JOINED
  end

  def left?
    action == LEFT
  end

209
210
211
212
  def expired?
    action == EXPIRED
  end

213
214
215
216
  def destroyed?
    action == DESTROYED
  end

217
218
219
220
221
  def commented?
    action == COMMENTED
  end

  def membership_changed?
222
    joined? || left? || expired?
223
224
  end

225
  def created_project?
226
    created? && !target && target_type.nil?
227
228
229
230
231
232
  end

  def created_target?
    created? && target
  end

233
234
235
236
237
  def milestone?
    target_type == "Milestone"
  end

  def note?
238
    target.is_a?(Note)
239
240
  end

241
  def issue?
242
    target_type == "Issue"
243
244
  end

245
  def merge_request?
246
    target_type == "MergeRequest"
247
248
  end

249
250
  def milestone
    target if milestone?
Alex Denisov's avatar
Alex Denisov committed
251
252
  end

253
  def issue
254
    target if issue?
255
256
257
  end

  def merge_request
258
    target if merge_request?
259
260
  end

261
  def note
262
    target if note?
263
264
  end

265
  def action_name
266
    if push?
267
      push_action_name
268
    elsif closed?
269
270
      "closed"
    elsif merged?
271
      "accepted"
Alex Denisov's avatar
Alex Denisov committed
272
273
    elsif joined?
      'joined'
Alex Denisov's avatar
Alex Denisov committed
274
275
    elsif left?
      'left'
276
277
    elsif expired?
      'removed due to membership expiration from'
278
279
    elsif destroyed?
      'destroyed'
280
281
    elsif commented?
      "commented on"
282
    elsif created_project?
283
      created_project_action_name
284
    else
285
      "opened"
286
287
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
288

289
290
291
292
  def target_iid
    target.respond_to?(:iid) ? target.iid : target_id
  end

293
  def commit_note?
294
    note? && target && target.for_commit?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
295
296
  end

297
  def issue_note?
298
    note? && target && target.for_issue?
299
300
  end

Valery Sizov's avatar
Valery Sizov committed
301
302
303
304
  def merge_request_note?
    note? && target && target.for_merge_request?
  end

305
  def project_snippet_note?
306
    note? && target && target.for_snippet?
Andrew8xx8's avatar
Andrew8xx8 committed
307
308
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
309
310
311
312
313
  def note_target
    target.noteable
  end

  def note_target_id
314
    if commit_note?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
315
316
317
318
319
320
      target.commit_id
    else
      target.noteable_id.to_s
    end
  end

321
322
323
324
325
326
  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
327
    else
328
329
      note_target.to_reference
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
330
331
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
332
333
334
335
336
337
338
  def note_target_type
    if target.noteable_type.present?
      target.noteable_type.titleize
    else
      "Wall"
    end.downcase
  end
339
340
341

  def body?
    if push?
342
      push_with_commits?
343
344
345
346
347
348
    elsif note?
      true
    else
      target.respond_to? :title
    end
  end
349
350

  def reset_project_activity
351
352
    return unless project

353
    # Don't bother updating if we know the project was updated recently.
354
    return if recent_update?
355

356
357
358
    # 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.
359
360
361
    Project.unscoped.where(id: project_id)
      .where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago)
      .update_all(last_activity_at: created_at)
362
363
  end

364
365
366
367
  def authored_by?(user)
    user ? author_id == user.id : false
  end

368
369
370
371
372
373
  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

374
375
  private

376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
  def push_action_name
    if new_ref?
      "pushed new"
    elsif rm_ref?
      "deleted"
    else
      "pushed to"
    end
  end

  def created_project_action_name
    if project.external_import?
      "imported"
    else
      "created"
    end
  end

394
395
396
  def recent_update?
    project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
  end
397
398

  def set_last_repository_updated_at
399
    Project.unscoped.where(id: project_id)
400
      .where("last_repository_updated_at < ? OR last_repository_updated_at IS NULL", REPOSITORY_UPDATED_AT_INTERVAL.ago)
401
      .update_all(last_repository_updated_at: created_at)
402
  end
403

404
  def track_user_interacted_projects
405
406
407
    # Note the call to .available? is due to earlier migrations
    # that would otherwise conflict with the call to .track
    # (because the table does not exist yet).
Andreas Brandl's avatar
Andreas Brandl committed
408
    UserInteractedProject.track(self) if UserInteractedProject.available?
409
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
410
end