BigW Consortium Gitlab

note.rb 7.35 KB
Newer Older
gitlabhq committed
1
class Note < ActiveRecord::Base
2
  extend ActiveModel::Naming
3
  include Gitlab::CurrentSettings
4
  include Participable
5
  include Mentionable
6
  include Awardable
7
  include Importable
8
  include FasterCacheKeys
9 10 11
  include CacheMarkdownField

  cache_markdown_field :note, pipeline: :note
12

13 14
  # Attribute containing rendered and redacted Markdown as generated by
  # Banzai::ObjectRenderer.
15
  attr_accessor :redacted_note_html
16

17 18 19 20
  # An Array containing the number of visible references as generated by
  # Banzai::ObjectRenderer
  attr_accessor :user_visible_reference_count

21 22
  default_value_for :system, false

Yorick Peterse committed
23
  attr_mentionable :note, pipeline: :note
24
  participant :author
25

gitlabhq committed
26
  belongs_to :project
27
  belongs_to :noteable, polymorphic: true, touch: true
28
  belongs_to :author, class_name: "User"
29
  belongs_to :updated_by, class_name: "User"
gitlabhq committed
30

31 32 33
  # Only used by DiffNote, but defined here so that it can be used in `Note.includes`
  belongs_to :resolved_by, class_name: "User"

34
  has_many :todos, dependent: :destroy
35
  has_many :events, as: :target, dependent: :destroy
36

37
  delegate :gfm_reference, :local_reference, to: :noteable
38 39
  delegate :name, to: :project, prefix: true
  delegate :name, :email, to: :author, prefix: true
40
  delegate :title, to: :noteable, allow_nil: true
41

42
  validates :note, :project, presence: true
43

44 45
  # Attachments are deprecated and are handled by Markdown uploader
  validates :attachment, file_size: { maximum: :max_attachment_size }
gitlabhq committed
46

47
  validates :noteable_type, presence: true
48
  validates :noteable_id, presence: true, unless: [:for_commit?, :importing?]
49
  validates :commit_id, presence: true, if: :for_commit?
Valery Sizov committed
50
  validates :author, presence: true
51

52
  validate unless: [:for_commit?, :importing?] do |note|
53
    unless note.noteable.try(:project) == note.project
54
      errors.add(:invalid_project, 'Note and noteable project mismatch')
55 56 57
    end
  end

58
  mount_uploader :attachment, AttachmentUploader
Andrey Kumanyaev committed
59 60

  # Scopes
61
  scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
62
  scope :system, ->{ where(system: true) }
63
  scope :user, ->{ where(system: false) }
64
  scope :common, ->{ where(noteable_type: ["", nil]) }
65
  scope :fresh, ->{ order(created_at: :asc, id: :asc) }
66 67
  scope :inc_author_project, ->{ includes(:project, :author) }
  scope :inc_author, ->{ includes(:author) }
68
  scope :inc_relations_for_view, ->{ includes(:project, :author, :updated_by, :resolved_by, :award_emoji) }
gitlabhq committed
69

Douwe Maan committed
70
  scope :diff_notes, ->{ where(type: ['LegacyDiffNote', 'DiffNote']) }
71 72
  scope :non_diff_notes, ->{ where(type: ['Note', nil]) }

73
  scope :with_associations, -> do
74 75
    # FYI noteable cannot be loaded for LegacyDiffNote for commits
    includes(:author, :noteable, :updated_by,
76
             project: [:project_members, { group: [:group_members] }])
77
  end
gitlabhq committed
78

79
  after_initialize :ensure_discussion_id
80
  before_validation :nullify_blank_type, :nullify_blank_line_code
81
  before_validation :set_discussion_id
82
  after_save :keep_around_commit
83

84
  class << self
85 86 87
    def model_name
      ActiveModel::Name.new(self, nil, 'note')
    end
88

89 90
    def build_discussion_id(noteable_type, noteable_id)
      [:discussion, noteable_type.try(:underscore), noteable_id].join("-")
91
    end
92

93
    def discussion_id(*args)
94 95 96
      Digest::SHA1.hexdigest(build_discussion_id(*args))
    end

97
    def discussions
98
      Discussion.for_notes(all)
99
    end
100

101
    def grouped_diff_discussions
102 103 104
      active_notes = diff_notes.fresh.select(&:active?)
      Discussion.for_diff_notes(active_notes).
        map { |d| [d.line_code, d] }.to_h
105 106
    end

107 108 109 110
    # Searches for notes matching the given query.
    #
    # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
    #
111 112
    # query   - The search query as a String.
    # as_user - Limit results to those viewable by a specific user
113 114
    #
    # Returns an ActiveRecord::Relation.
115
    def search(query, as_user: nil)
116
      table   = arel_table
117 118
      pattern = "%#{query}%"

119 120 121
      Note.joins('LEFT JOIN issues ON issues.id = noteable_id').
        where(table[:note].matches(pattern)).
        merge(Issue.visible_to_user(as_user))
122
    end
123
  end
124

125
  def cross_reference?
126
    system && SystemNoteService.cross_reference?(note)
127 128
  end

129 130
  def diff_note?
    false
131 132
  end

133 134
  def legacy_diff_note?
    false
135 136
  end

Douwe Maan committed
137 138 139 140
  def new_diff_note?
    false
  end

141
  def active?
142
    true
143 144
  end

145 146 147 148 149 150 151 152 153 154 155 156
  def resolvable?
    false
  end

  def resolved?
    false
  end

  def to_be_resolved?
    resolvable? && !resolved?
  end

157 158
  def max_attachment_size
    current_application_settings.max_attachment_size.megabytes.to_i
159 160
  end

161 162
  def hook_attrs
    attributes
163 164 165 166 167 168
  end

  def for_commit?
    noteable_type == "Commit"
  end

Riyad Preukschas committed
169 170 171 172
  def for_issue?
    noteable_type == "Issue"
  end

173 174 175 176
  def for_merge_request?
    noteable_type == "MergeRequest"
  end

177
  def for_snippet?
178 179 180
    noteable_type == "Snippet"
  end

181 182 183
  # override to return commits, which are not active record
  def noteable
    if for_commit?
184
      project.commit(commit_id)
185
    else
186
      super
187
    end
188 189
  # Temp fix to prevent app crash
  # if note commit id doesn't exist
190
  rescue
191
    nil
192
  end
193

Andrew8xx8 committed
194
  # FIXME: Hack for polymorphic associations with STI
Steven Burgart committed
195
  #        For more information visit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations
196 197
  def noteable_type=(noteable_type)
    super(noteable_type.to_s.classify.constantize.base_class.to_s)
Andrew8xx8 committed
198
  end
199 200 201 202 203 204 205 206 207 208 209

  # Reset notes events cache
  #
  # Since we do cache @event we need to reset cache in special cases:
  # * when a note is updated
  # * when a note is removed
  # Events cache stored like  events/23-20130109142513.
  # The cache key includes updated_at timestamp.
  # Thus it will automatically generate a new fragment
  # when the event is updated because the key changes.
  def reset_events_cache
210
    Event.reset_event_cache_for(self)
211
  end
212

213
  def editable?
214
    !system?
215
  end
216

217
  def cross_reference_not_visible_for?(user)
218 219 220 221 222 223 224 225 226
    cross_reference? && !has_referenced_mentionables?(user)
  end

  def has_referenced_mentionables?(user)
    if user_visible_reference_count.present?
      user_visible_reference_count > 0
    else
      referenced_mentionables(user).any?
    end
227 228
  end

229
  def award_emoji?
230
    can_be_award_emoji? && contains_emoji_only?
231 232
  end

233 234 235 236
  def emoji_awardable?
    !system?
  end

237
  def can_be_award_emoji?
238
    noteable.is_a?(Awardable)
239 240
  end

241
  def contains_emoji_only?
242
    note =~ /\A#{Banzai::Filter::EmojiFilter.emoji_pattern}\s?\Z/
243 244 245
  end

  def award_emoji_name
246
    note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
247
  end
248 249 250 251 252 253

  private

  def keep_around_commit
    project.repository.keep_around(self.commit_id)
  end
254 255 256 257 258 259 260 261

  def nullify_blank_type
    self.type = nil if self.type.blank?
  end

  def nullify_blank_line_code
    self.line_code = nil if self.line_code.blank?
  end
262 263 264

  def ensure_discussion_id
    return unless self.persisted?
265 266
    # Needed in case the SELECT statement doesn't ask for `discussion_id`
    return unless self.has_attribute?(:discussion_id)
267 268 269 270 271 272 273 274 275 276 277 278
    return if self.discussion_id

    set_discussion_id
    update_column(:discussion_id, self.discussion_id)
  end

  def set_discussion_id
    self.discussion_id = Digest::SHA1.hexdigest(build_discussion_id)
  end

  def build_discussion_id
    if for_merge_request?
Douwe Maan committed
279 280 281
      # Notes on merge requests are always in a discussion of their own,
      # so we generate a unique discussion ID.
      [:discussion, :note, SecureRandom.hex].join("-")
282 283 284 285
    else
      self.class.build_discussion_id(noteable_type, noteable_id || commit_id)
    end
  end
gitlabhq committed
286
end