event.rb 8.06 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) }
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
  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)
75
      .preload(:push_event_payload, target: :author)
76 77
  end

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

80 81 82 83 84 85
  # 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

86 87
  self.inheritance_column = 'action'

88 89 90 91
  # "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
92
  class << self
93 94 95 96
    def model_name
      ActiveModel::Name.new(self, nil, 'event')
    end

97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
    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

115
    # Update Gitlab::ContributionsCalendar#activity_dates if this changes
116
    def contributions
117 118
      where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)",
            Event::PUSHED,
Douwe Maan's avatar
Douwe Maan committed
119
            %w(MergeRequest Issue), [Event::CREATED, Event::CLOSED, Event::MERGED],
120
            "Note", Event::COMMENTED)
121
    end
122

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

    def actions
      ACTIONS.keys
    end

    def target_types
      TARGET_TYPES.keys
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
134 135
  end

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

152 153
  def project_name
    if project
154
      project.name_with_namespace
155
    else
Riyad Preukschas's avatar
Riyad Preukschas committed
156
      "(deleted project)"
157 158 159
    end
  end

160
  def target_title
161
    target.try(:title)
162 163 164 165
  end

  def created?
    action == CREATED
166 167
  end

168
  def push?
169
    false
170 171
  end

172
  def merged?
173
    action == MERGED
174 175
  end

176
  def closed?
177
    action == CLOSED
178 179 180
  end

  def reopened?
181 182 183 184 185 186 187 188 189 190 191
    action == REOPENED
  end

  def joined?
    action == JOINED
  end

  def left?
    action == LEFT
  end

192 193 194 195
  def expired?
    action == EXPIRED
  end

196 197 198 199
  def destroyed?
    action == DESTROYED
  end

200 201 202 203 204
  def commented?
    action == COMMENTED
  end

  def membership_changed?
205
    joined? || left? || expired?
206 207
  end

208
  def created_project?
209
    created? && !target && target_type.nil?
210 211 212 213 214 215
  end

  def created_target?
    created? && target
  end

216 217 218 219 220
  def milestone?
    target_type == "Milestone"
  end

  def note?
221
    target.is_a?(Note)
222 223
  end

224
  def issue?
225
    target_type == "Issue"
226 227
  end

228
  def merge_request?
229
    target_type == "MergeRequest"
230 231
  end

232 233
  def milestone
    target if milestone?
234 235
  end

236
  def issue
237
    target if issue?
238 239 240
  end

  def merge_request
241
    target if merge_request?
242 243
  end

244
  def note
245
    target if note?
246 247
  end

248
  def action_name
249
    if push?
250
      push_action_name
251
    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
      created_project_action_name
267
    else
268
      "opened"
269 270
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
271

272 273 274 275
  def target_iid
    target.respond_to?(:iid) ? target.iid : target_id
  end

276
  def commit_note?
277
    note? && target && target.for_commit?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
278 279
  end

280
  def issue_note?
281
    note? && target && target.for_issue?
282 283
  end

284 285 286 287
  def merge_request_note?
    note? && target && target.for_merge_request?
  end

288
  def project_snippet_note?
289
    note? && target && target.for_snippet?
Andrew8xx8's avatar
Andrew8xx8 committed
290 291
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
292 293 294 295 296
  def note_target
    target.noteable
  end

  def note_target_id
297
    if commit_note?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
298 299 300 301 302 303
      target.commit_id
    else
      target.noteable_id.to_s
    end
  end

304 305 306 307 308 309
  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
310
    else
311 312
      note_target.to_reference
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
313 314
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
315 316 317 318 319 320 321
  def note_target_type
    if target.noteable_type.present?
      target.noteable_type.titleize
    else
      "Wall"
    end.downcase
  end
322 323 324

  def body?
    if push?
325
      push_with_commits?
326 327 328 329 330 331
    elsif note?
      true
    else
      target.respond_to? :title
    end
  end
332 333

  def reset_project_activity
334 335
    return unless project

336
    # Don't bother updating if we know the project was updated recently.
337
    return if recent_update?
338

339 340 341
    # 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.
342 343 344
    Project.unscoped.where(id: project_id)
      .where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago)
      .update_all(last_activity_at: created_at)
345 346
  end

347 348 349 350
  def authored_by?(user)
    user ? author_id == user.id : false
  end

351 352 353 354 355 356
  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

357 358
  private

359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376
  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

377 378 379
  def recent_update?
    project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
  end
380 381

  def set_last_repository_updated_at
382 383
    Project.unscoped.where(id: project_id)
      .update_all(last_repository_updated_at: created_at)
384
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
385
end