event.rb 9.21 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
  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).
90
    includes(:author, :project, project: [:project_feature, :import_data, :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
    # Update Gitlab::ContributionsCalendar#activity_dates if this changes
118
    def contributions
119 120
      where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)",
            Event::PUSHED,
Douwe Maan's avatar
Douwe Maan committed
121
            %w(MergeRequest Issue), [Event::CREATED, Event::CLOSED, Event::MERGED],
122
            "Note", Event::COMMENTED)
123
    end
124

Yorick Peterse's avatar
Yorick Peterse committed
125 126 127
    def limit_recent(limit = 20, offset = nil)
      recent.limit(limit).offset(offset)
    end
Mark Fletcher's avatar
Mark Fletcher committed
128 129 130 131 132 133 134 135

    def actions
      ACTIONS.keys
    end

    def target_types
      TARGET_TYPES.keys
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
136 137
  end

138 139
  # rubocop:disable Metrics/CyclomaticComplexity
  # rubocop:disable Metrics/PerceivedComplexity
140
  def visible_to_user?(user = nil)
141
    if push? || commit_note?
142
      Ability.allowed?(user, :download_code, project)
143
    elsif membership_changed?
144
      Ability.allowed?(user, :read_project, project)
145
    elsif created_project?
146
      Ability.allowed?(user, :read_project, project)
147
    elsif issue? || issue_note?
http://jneen.net/'s avatar
http://jneen.net/ committed
148
      Ability.allowed?(user, :read_issue, note? ? note_target : target)
149 150
    elsif merge_request? || merge_request_note?
      Ability.allowed?(user, :read_merge_request, note? ? note_target : target)
151 152 153 154
    elsif personal_snippet_note?
      Ability.allowed?(user, :read_personal_snippet, note_target)
    elsif project_snippet_note?
      Ability.allowed?(user, :read_project_snippet, note_target)
155
    elsif milestone?
156
      Ability.allowed?(user, :read_milestone, project)
157
    else
158
      false # No other event types are visible
159
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
160
  end
161 162
  # rubocop:enable Metrics/PerceivedComplexity
  # rubocop:enable Metrics/CyclomaticComplexity
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
163

164 165
  def project_name
    if project
166
      project.full_name
167
    else
Riyad Preukschas's avatar
Riyad Preukschas committed
168
      "(deleted project)"
169 170 171
    end
  end

172
  def target_title
173
    target.try(:title)
174 175 176 177
  end

  def created?
    action == CREATED
178 179
  end

180
  def push?
181
    false
182 183
  end

184
  def merged?
185
    action == MERGED
186 187
  end

188
  def closed?
189
    action == CLOSED
190 191 192
  end

  def reopened?
193 194 195 196 197 198 199 200 201 202 203
    action == REOPENED
  end

  def joined?
    action == JOINED
  end

  def left?
    action == LEFT
  end

204 205 206 207
  def expired?
    action == EXPIRED
  end

208 209 210 211
  def destroyed?
    action == DESTROYED
  end

212 213 214 215 216
  def commented?
    action == COMMENTED
  end

  def membership_changed?
217
    joined? || left? || expired?
218 219
  end

220
  def created_project?
221
    created? && !target && target_type.nil?
222 223 224 225 226 227
  end

  def created_target?
    created? && target
  end

228 229 230 231 232
  def milestone?
    target_type == "Milestone"
  end

  def note?
233
    target.is_a?(Note)
234 235
  end

236
  def issue?
237
    target_type == "Issue"
238 239
  end

240
  def merge_request?
241
    target_type == "MergeRequest"
242 243
  end

244 245
  def milestone
    target if milestone?
246 247
  end

248
  def issue
249
    target if issue?
250 251 252
  end

  def merge_request
253
    target if merge_request?
254 255
  end

256
  def note
257
    target if note?
258 259
  end

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

284 285 286 287
  def target_iid
    target.respond_to?(:iid) ? target.iid : target_id
  end

288
  def commit_note?
289
    note? && target && target.for_commit?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
290 291
  end

292
  def issue_note?
293
    note? && target && target.for_issue?
294 295
  end

296 297 298 299
  def merge_request_note?
    note? && target && target.for_merge_request?
  end

300
  def project_snippet_note?
301
    note? && target && target.for_snippet?
Andrew8xx8's avatar
Andrew8xx8 committed
302 303
  end

304 305 306 307
  def personal_snippet_note?
    note? && target && target.for_personal_snippet?
  end

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

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

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

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

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

  def reset_project_activity
350 351
    return unless project

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

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

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

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

373 374
  private

375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
  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

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

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

403
  def track_user_interacted_projects
404 405 406
    # 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
407
    UserInteractedProject.track(self) if UserInteractedProject.available?
408
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
409
end