BigW Consortium Gitlab

note.rb 7.27 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 12 13
  # Attribute containing rendered and redacted Markdown as generated by
  # Banzai::ObjectRenderer.
  attr_accessor :note_html

14 15 16 17
  # An Array containing the number of visible references as generated by
  # Banzai::ObjectRenderer
  attr_accessor :user_visible_reference_count

18 19
  default_value_for :system, false

Yorick Peterse committed
20
  attr_mentionable :note, pipeline: :note
21
  participant :author
22

gitlabhq committed
23
  belongs_to :project
24
  belongs_to :noteable, polymorphic: true, touch: true
25
  belongs_to :author, class_name: "User"
26
  belongs_to :updated_by, class_name: "User"
gitlabhq committed
27

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

31
  has_many :todos, dependent: :destroy
32
  has_many :events, as: :target, dependent: :destroy
33

34
  delegate :gfm_reference, :local_reference, to: :noteable
35 36
  delegate :name, to: :project, prefix: true
  delegate :name, :email, to: :author, prefix: true
37
  delegate :title, to: :noteable, allow_nil: true
38

39
  validates :note, :project, presence: true
40

41 42
  # Attachments are deprecated and are handled by Markdown uploader
  validates :attachment, file_size: { maximum: :max_attachment_size }
gitlabhq committed
43

44
  validates :noteable_type, presence: true
45
  validates :noteable_id, presence: true, unless: [:for_commit?, :importing?]
46
  validates :commit_id, presence: true, if: :for_commit?
Valery Sizov committed
47
  validates :author, presence: true
48

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

55
  mount_uploader :attachment, AttachmentUploader
Andrey Kumanyaev committed
56 57

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

Douwe Maan committed
67
  scope :diff_notes, ->{ where(type: ['LegacyDiffNote', 'DiffNote']) }
68 69
  scope :non_diff_notes, ->{ where(type: ['Note', nil]) }

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

76
  after_initialize :ensure_discussion_id
77
  before_validation :nullify_blank_type, :nullify_blank_line_code
78
  before_validation :set_discussion_id
79
  after_save :keep_around_commit
80

81
  class << self
82 83 84
    def model_name
      ActiveModel::Name.new(self, nil, 'note')
    end
85

86 87
    def build_discussion_id(noteable_type, noteable_id)
      [:discussion, noteable_type.try(:underscore), noteable_id].join("-")
88
    end
89

90
    def discussion_id(*args)
91 92 93
      Digest::SHA1.hexdigest(build_discussion_id(*args))
    end

94
    def discussions
95
      Discussion.for_notes(all)
96
    end
97

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

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

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

122
  def cross_reference?
123
    system && SystemNoteService.cross_reference?(note)
124 125
  end

126 127
  def diff_note?
    false
128 129
  end

130 131
  def legacy_diff_note?
    false
132 133
  end

Douwe Maan committed
134 135 136 137
  def new_diff_note?
    false
  end

138
  def active?
139
    true
140 141
  end

142 143 144 145 146 147 148 149 150 151 152 153
  def resolvable?
    false
  end

  def resolved?
    false
  end

  def to_be_resolved?
    resolvable? && !resolved?
  end

154 155
  def max_attachment_size
    current_application_settings.max_attachment_size.megabytes.to_i
156 157
  end

158 159
  def hook_attrs
    attributes
160 161 162 163 164 165
  end

  def for_commit?
    noteable_type == "Commit"
  end

Riyad Preukschas committed
166 167 168 169
  def for_issue?
    noteable_type == "Issue"
  end

170 171 172 173
  def for_merge_request?
    noteable_type == "MergeRequest"
  end

174
  def for_snippet?
175 176 177
    noteable_type == "Snippet"
  end

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

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

  # 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
207
    Event.reset_event_cache_for(self)
208
  end
209

210
  def editable?
211
    !system?
212
  end
213

214
  def cross_reference_not_visible_for?(user)
215 216 217 218 219 220 221 222 223
    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
224 225
  end

226
  def award_emoji?
227
    can_be_award_emoji? && contains_emoji_only?
228 229
  end

230 231 232 233
  def emoji_awardable?
    !system?
  end

234
  def can_be_award_emoji?
235
    noteable.is_a?(Awardable)
236 237
  end

238
  def contains_emoji_only?
239
    note =~ /\A#{Banzai::Filter::EmojiFilter.emoji_pattern}\s?\Z/
240 241 242
  end

  def award_emoji_name
243
    note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
244
  end
245 246 247 248 249 250

  private

  def keep_around_commit
    project.repository.keep_around(self.commit_id)
  end
251 252 253 254 255 256 257 258

  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
259 260 261

  def ensure_discussion_id
    return unless self.persisted?
262 263
    # Needed in case the SELECT statement doesn't ask for `discussion_id`
    return unless self.has_attribute?(:discussion_id)
264 265 266 267 268 269 270 271 272 273 274 275
    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
276 277 278
      # Notes on merge requests are always in a discussion of their own,
      # so we generate a unique discussion ID.
      [:discussion, :note, SecureRandom.hex].join("-")
279 280 281 282
    else
      self.class.build_discussion_id(noteable_type, noteable_id || commit_id)
    end
  end
gitlabhq committed
283
end