summaryrefslogtreecommitdiff
path: root/app/models/concerns/noteable.rb
blob: eed396f785b3f7bbd041b47b140e33afa7f2451e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# frozen_string_literal: true

module Noteable
  extend ActiveSupport::Concern

  # This object is used to gather noteable meta data for list displays
  # avoiding n+1 queries and improving performance.
  NoteableMeta = Struct.new(:user_notes_count)

  MAX_NOTES_LIMIT = 5_000

  class_methods do
    # `Noteable` class names that support replying to individual notes.
    def replyable_types
      %w(Issue MergeRequest)
    end

    # `Noteable` class names that support resolvable notes.
    def resolvable_types
      %w(MergeRequest DesignManagement::Design)
    end

    # `Noteable` class names that support creating/forwarding individual notes.
    def email_creatable_types
      %w(Issue)
    end
  end

  # The timestamp of the note (e.g. the :created_at or :updated_at attribute if provided via
  # API call)
  def system_note_timestamp
    @system_note_timestamp || Time.current # rubocop:disable Gitlab/ModuleWithInstanceVariables
  end

  attr_writer :system_note_timestamp

  def base_class_name
    self.class.base_class.name
  end

  # Convert this Noteable class name to a format usable by notifications.
  #
  # Examples:
  #
  #   noteable.class           # => MergeRequest
  #   noteable.human_class_name # => "merge request"
  def human_class_name
    @human_class_name ||= base_class_name.titleize.downcase
  end

  def supports_resolvable_notes?
    self.class.resolvable_types.include?(base_class_name)
  end

  def supports_discussions?
    DiscussionNote.noteable_types.include?(base_class_name)
  end

  def supports_replying_to_individual_notes?
    supports_discussions? && self.class.replyable_types.include?(base_class_name)
  end

  def supports_creating_notes_by_email?
    self.class.email_creatable_types.include?(base_class_name)
  end

  def supports_suggestion?
    false
  end

  def discussions_rendered_on_frontend?
    false
  end

  def preloads_discussion_diff_highlighting?
    false
  end

  def has_any_diff_note_positions?
    notes.any? && DiffNotePosition.where(note: notes).exists?
  end

  def discussion_notes
    notes
  end

  delegate :find_discussion, to: :discussion_notes

  def discussions
    @discussions ||= discussion_notes
      .inc_relations_for_view(self)
      .discussions(self)
  end

  def discussion_ids_relation
    notes.select(:discussion_id)
      .group(:discussion_id)
      .order('MIN(created_at), MIN(id)')
  end

  # This does not consider OutOfContextDiscussions in MRs
  # where notes from commits are overriden so that they have
  # the same discussion_id
  def discussion_root_note_ids(notes_filter:)
    relations = []

    relations << discussion_notes.select(
      "'notes' AS table_name",
      'discussion_id',
      'MIN(id) AS id',
      'MIN(created_at) AS created_at'
    ).with_notes_filter(notes_filter)
     .group(:discussion_id)

    if notes_filter != UserPreference::NOTES_FILTERS[:only_comments]
      relations += synthetic_note_ids_relations
    end

    Note.from_union(relations, remove_duplicates: false).fresh
  end

  def capped_notes_count(max)
    notes.limit(max).count
  end

  def grouped_diff_discussions(*args)
    # Doesn't use `discussion_notes`, because this may include commit diff notes
    # besides MR diff notes, that we do not want to display on the MR Changes tab.
    notes.inc_relations_for_view(self).grouped_diff_discussions(*args)
  end

  # rubocop:disable Gitlab/ModuleWithInstanceVariables
  def resolvable_discussions
    @resolvable_discussions ||=
      if defined?(@discussions)
        @discussions.select(&:resolvable?)
      else
        discussion_notes.resolvable.discussions(self)
      end
  end
  # rubocop:enable Gitlab/ModuleWithInstanceVariables

  def discussions_resolvable?
    resolvable_discussions.any?(&:resolvable?)
  end

  def discussions_resolved?
    discussions_resolvable? && resolvable_discussions.none?(&:to_be_resolved?)
  end

  def discussions_to_be_resolved
    @discussions_to_be_resolved ||= resolvable_discussions.select(&:to_be_resolved?)
  end

  def discussions_can_be_resolved_by?(user)
    discussions_to_be_resolved.all? { |discussion| discussion.can_resolve?(user) }
  end

  def lockable?
    [MergeRequest, Issue].include?(self.class)
  end

  def etag_caching_enabled?
    false
  end

  def expire_note_etag_cache
    return unless discussions_rendered_on_frontend?
    return unless etag_caching_enabled?

    Gitlab::EtagCaching::Store.new.touch(note_etag_key)
  end

  def note_etag_key
    return Gitlab::Routing.url_helpers.designs_project_issue_path(project, issue, { vueroute: filename }) if self.is_a?(DesignManagement::Design)

    Gitlab::Routing.url_helpers.project_noteable_notes_path(
      project,
      target_type: noteable_target_type_name,
      target_id: id
    )
  end

  def after_note_created(_note)
    # no-op
  end

  def after_note_destroyed(_note)
    # no-op
  end

  # Email address that an authorized user can send/forward an email to be added directly
  # to an issue or merge request.
  # example: incoming+h5bp-html5-boilerplate-8-1234567890abcdef123456789-issue-34@localhost.com
  def creatable_note_email_address(author)
    return unless supports_creating_notes_by_email?

    project_email = project.new_issuable_address(author, self.class.name.underscore)
    return unless project_email

    project_email.sub('@', "-#{iid}@")
  end

  def noteable_target_type_name
    model_name.singular
  end

  def commenters(user: nil)
    eligable_notes = notes.user

    eligable_notes = eligable_notes.not_internal unless user&.can?(:read_internal_note, self)

    User.where(id: eligable_notes.select(:author_id).distinct)
  end

  private

  # Synthetic system notes don't have discussion IDs because these are generated dynamically
  # in Ruby. These are always root notes anyway so we don't need to group by discussion ID.
  def synthetic_note_ids_relations
    relations = []

    # currently multiple models include Noteable concern, but not all of them support
    # all resource events, so we check if given model supports given resource event.
    if respond_to?(:resource_label_events)
      relations << resource_label_events.select("'resource_label_events'", "'NULL'", :id, :created_at)
    end

    if respond_to?(:resource_state_events)
      relations << resource_state_events.select("'resource_state_events'", "'NULL'", :id, :created_at)
    end

    if respond_to?(:resource_milestone_events)
      relations << resource_milestone_events.select("'resource_milestone_events'", "'NULL'", :id, :created_at)
    end

    relations
  end
end

Noteable.extend(Noteable::ClassMethods)

Noteable::ClassMethods.prepend_mod_with('Noteable::ClassMethods')
Noteable.prepend_mod_with('Noteable')