2018-07-25 09:30:33 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-04-04 22:27:23 +00:00
|
|
|
# A non-diff discussion on an issue, merge request, commit, or snippet, consisting of `DiscussionNote` notes.
|
2017-04-06 15:05:57 +00:00
|
|
|
#
|
|
|
|
# A discussion of this type can be resolvable.
|
2016-07-20 22:18:18 +00:00
|
|
|
class Discussion
|
2019-06-07 17:13:26 +00:00
|
|
|
include GlobalID::Identification
|
2017-03-31 00:38:21 +00:00
|
|
|
include ResolvableDiscussion
|
2016-07-20 22:18:18 +00:00
|
|
|
|
2021-07-08 09:09:33 +00:00
|
|
|
# Bump this if we need to refresh the cached versions of discussions
|
|
|
|
CACHE_VERSION = 1
|
|
|
|
|
2017-04-07 16:29:29 +00:00
|
|
|
attr_reader :notes, :context_noteable
|
2016-07-20 22:18:18 +00:00
|
|
|
|
|
|
|
delegate :created_at,
|
|
|
|
:project,
|
|
|
|
:author,
|
|
|
|
:noteable,
|
2017-06-29 21:19:09 +00:00
|
|
|
:commit_id,
|
2020-03-17 00:09:12 +00:00
|
|
|
:confidential?,
|
2016-07-20 22:18:18 +00:00
|
|
|
:for_commit?,
|
2020-11-05 06:08:56 +00:00
|
|
|
:for_design?,
|
2016-07-20 22:18:18 +00:00
|
|
|
:for_merge_request?,
|
2019-10-17 03:55:04 +00:00
|
|
|
:noteable_ability_name,
|
2019-06-07 17:13:26 +00:00
|
|
|
:to_ability_name,
|
|
|
|
:editable?,
|
2020-06-01 00:08:25 +00:00
|
|
|
:resolved_by_id,
|
2020-02-24 03:09:05 +00:00
|
|
|
:system_note_with_references_visible_for?,
|
|
|
|
:resource_parent,
|
2020-08-18 21:09:57 +00:00
|
|
|
:save,
|
2016-07-20 22:18:18 +00:00
|
|
|
to: :first_note
|
|
|
|
|
2019-06-07 17:13:26 +00:00
|
|
|
def declarative_policy_delegate
|
|
|
|
first_note
|
|
|
|
end
|
|
|
|
|
2018-06-21 12:22:40 +00:00
|
|
|
def project_id
|
|
|
|
project&.id
|
|
|
|
end
|
|
|
|
|
2017-04-07 16:29:29 +00:00
|
|
|
def self.build(notes, context_noteable = nil)
|
|
|
|
notes.first.discussion_class(context_noteable).new(notes, context_noteable)
|
2017-03-10 01:29:11 +00:00
|
|
|
end
|
Change diff highlight/truncate for reusability
Previously the `truncated_diff_lines` method for outputting a discussion diff took in already highlighted lines, which meant it wasn't reuseable for truncating ANY lines. In the way it was used, it also meant that for any email truncation, the whole diff was being highlighted before being truncated, meaning wasted time highlighting lines that wouldn't even be used (granted, they were being memoized, so perhaps this wasn't that great of an issue). I refactored truncation away from highlighting, in order to truncate formatted diffs for text templates in email, using `>`s to designate each line, but otherwise retaining the parsing already done to create `diff_lines`.
Additionally, while notes on merge requests or commits had already been tested, there was no existing test for notes on a diff on an MR or commit. Added mailer tests for such, and a unit test for truncating diff lines.
2016-08-25 16:38:07 +00:00
|
|
|
|
2017-04-07 16:29:29 +00:00
|
|
|
def self.build_collection(notes, context_noteable = nil)
|
2017-05-31 19:00:30 +00:00
|
|
|
grouped_notes = notes.group_by { |n| n.discussion_id(context_noteable) }
|
2017-05-29 21:07:57 +00:00
|
|
|
grouped_notes.values.map { |notes| build(notes, context_noteable) }
|
2017-03-10 01:29:11 +00:00
|
|
|
end
|
2016-07-20 22:18:18 +00:00
|
|
|
|
2019-07-04 03:33:14 +00:00
|
|
|
def self.lazy_find(discussion_id)
|
|
|
|
BatchLoader.for(discussion_id).batch do |discussion_ids, loader|
|
|
|
|
results = Note.where(discussion_id: discussion_ids).fresh.to_a.group_by(&:discussion_id)
|
|
|
|
results.each do |discussion_id, notes|
|
|
|
|
next if notes.empty?
|
|
|
|
|
|
|
|
loader.call(discussion_id, Discussion.build(notes))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-04-04 22:27:23 +00:00
|
|
|
# Returns an alphanumeric discussion ID based on `build_discussion_id`
|
2017-03-10 01:29:11 +00:00
|
|
|
def self.discussion_id(note)
|
|
|
|
Digest::SHA1.hexdigest(build_discussion_id(note).join("-"))
|
2016-07-20 22:18:18 +00:00
|
|
|
end
|
|
|
|
|
2017-04-04 22:27:23 +00:00
|
|
|
# Returns an array of discussion ID components
|
|
|
|
def self.build_discussion_id(note)
|
|
|
|
[*base_discussion_id(note), SecureRandom.hex]
|
2016-07-20 22:18:18 +00:00
|
|
|
end
|
|
|
|
|
2017-04-04 22:27:23 +00:00
|
|
|
def self.base_discussion_id(note)
|
2017-03-10 01:29:11 +00:00
|
|
|
noteable_id = note.noteable_id || note.commit_id
|
|
|
|
[:discussion, note.noteable_type.try(:underscore), noteable_id]
|
2016-07-20 22:18:18 +00:00
|
|
|
end
|
|
|
|
|
2017-04-04 22:27:23 +00:00
|
|
|
# When notes on a commit are displayed in context of a merge request that contains that commit,
|
|
|
|
# these notes are to be displayed as if they were part of one discussion, even though they were actually
|
|
|
|
# individual notes on the commit with different discussion IDs, so that it's clear that these are not
|
|
|
|
# notes on the merge request itself.
|
2017-04-04 23:20:08 +00:00
|
|
|
#
|
|
|
|
# To turn a list of notes into a list of discussions, they are grouped by discussion ID, so to
|
|
|
|
# get these out-of-context notes to end up in the same discussion, we need to get them to return the same
|
2017-04-04 22:27:23 +00:00
|
|
|
# `discussion_id` when this grouping happens. To enable this, `Note#discussion_id` calls out
|
|
|
|
# to the `override_discussion_id` method on the appropriate `Discussion` subclass, as determined by
|
|
|
|
# the `discussion_class` method on `Note` or a subclass of `Note`.
|
2017-04-04 23:20:08 +00:00
|
|
|
#
|
2017-04-04 22:27:23 +00:00
|
|
|
# If no override is necessary, return `nil`.
|
|
|
|
# For the case described above, see `OutOfContextDiscussion.override_discussion_id`.
|
|
|
|
def self.override_discussion_id(note)
|
|
|
|
nil
|
2017-03-10 01:29:11 +00:00
|
|
|
end
|
|
|
|
|
2017-04-06 15:05:57 +00:00
|
|
|
def self.note_class
|
|
|
|
DiscussionNote
|
|
|
|
end
|
|
|
|
|
2017-04-07 16:29:29 +00:00
|
|
|
def initialize(notes, context_noteable = nil)
|
2017-03-10 01:29:11 +00:00
|
|
|
@notes = notes
|
2017-04-07 16:29:29 +00:00
|
|
|
@context_noteable = context_noteable
|
2016-07-29 03:14:45 +00:00
|
|
|
end
|
|
|
|
|
2017-10-07 04:25:17 +00:00
|
|
|
def on_image?
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2017-03-17 19:25:52 +00:00
|
|
|
def ==(other)
|
|
|
|
other.class == self.class &&
|
2017-04-07 16:29:29 +00:00
|
|
|
other.context_noteable == self.context_noteable &&
|
2017-03-17 19:25:52 +00:00
|
|
|
other.id == self.id &&
|
|
|
|
other.notes == self.notes
|
|
|
|
end
|
|
|
|
|
2016-07-26 04:49:21 +00:00
|
|
|
def last_updated_at
|
|
|
|
last_note.created_at
|
|
|
|
end
|
|
|
|
|
|
|
|
def last_updated_by
|
|
|
|
last_note.author
|
|
|
|
end
|
|
|
|
|
2017-06-09 21:24:54 +00:00
|
|
|
def updated?
|
|
|
|
last_updated_at != created_at
|
|
|
|
end
|
|
|
|
|
2016-07-20 22:18:18 +00:00
|
|
|
def id
|
2017-04-07 16:29:29 +00:00
|
|
|
first_note.discussion_id(context_noteable)
|
2016-07-20 22:18:18 +00:00
|
|
|
end
|
2016-08-17 17:14:44 +00:00
|
|
|
|
2017-06-01 14:05:21 +00:00
|
|
|
def reply_id
|
|
|
|
# To reply to this discussion, we need the actual discussion_id from the database,
|
|
|
|
# not the potentially overwritten one based on the noteable.
|
|
|
|
first_note.discussion_id
|
|
|
|
end
|
|
|
|
|
2016-08-15 23:45:23 +00:00
|
|
|
alias_method :to_param, :id
|
2016-07-20 22:18:18 +00:00
|
|
|
|
|
|
|
def diff_discussion?
|
2017-03-10 01:29:11 +00:00
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2017-03-17 19:25:52 +00:00
|
|
|
def individual_note?
|
2017-03-10 01:29:11 +00:00
|
|
|
false
|
2016-07-20 22:18:18 +00:00
|
|
|
end
|
|
|
|
|
2019-02-06 10:31:46 +00:00
|
|
|
def can_convert_to_discussion?
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2016-09-02 20:29:47 +00:00
|
|
|
def last_note
|
2017-03-10 01:29:11 +00:00
|
|
|
@last_note ||= notes.last
|
2016-09-02 20:29:47 +00:00
|
|
|
end
|
2016-07-26 04:37:22 +00:00
|
|
|
|
2016-07-26 04:46:13 +00:00
|
|
|
def collapsed?
|
2017-03-17 19:25:52 +00:00
|
|
|
resolved?
|
2016-07-26 04:46:13 +00:00
|
|
|
end
|
|
|
|
|
2016-07-20 22:18:18 +00:00
|
|
|
def expanded?
|
2016-07-26 04:46:13 +00:00
|
|
|
!collapsed?
|
2016-07-20 22:18:18 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def reply_attributes
|
2017-03-31 01:33:45 +00:00
|
|
|
first_note.slice(:type, :noteable_type, :noteable_id, :commit_id, :discussion_id)
|
2016-07-20 22:18:18 +00:00
|
|
|
end
|
2021-07-08 09:09:33 +00:00
|
|
|
|
|
|
|
def cache_key
|
|
|
|
# Need this so cache will be invalidated when note within a discussion
|
|
|
|
# has been deleted.
|
|
|
|
notes_sha = Digest::SHA1.hexdigest(notes.map(&:id).join(':'))
|
|
|
|
|
|
|
|
[
|
|
|
|
CACHE_VERSION,
|
|
|
|
notes.last.latest_cached_markdown_version,
|
|
|
|
id,
|
|
|
|
notes_sha,
|
|
|
|
notes.max_by(&:updated_at).updated_at,
|
|
|
|
resolved_at
|
|
|
|
].join(':')
|
|
|
|
end
|
2016-07-20 22:18:18 +00:00
|
|
|
end
|