Unverified Commit 21e10888 authored by Douwe Maan's avatar Douwe Maan Committed by Luke "Jared" Bennett
Browse files

Address review comments

parent fe26b8af
module RendersNotes
def prepare_notes_for_rendering(notes)
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
Banzai::NoteRenderer.render(notes, @project, current_user)
notes
end
private
def preload_max_access_for_authors(notes, project)
user_ids = notes.map(&:author_id)
project.team.max_member_access_for_user_ids(user_ids)
end
def preload_noteable_for_regular_notes(notes)
ActiveRecord::Associations::Preloader.new.preload(notes.reject(&:for_commit?), :noteable)
end
end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
# #
# Not to be confused with CommitsController, plural. # Not to be confused with CommitsController, plural.
class Projects::CommitController < Projects::ApplicationController class Projects::CommitController < Projects::ApplicationController
include NotesHelper include RendersNotes
include CreatesCommit include CreatesCommit
include DiffForPath include DiffForPath
include DiffHelper include DiffHelper
......
class Projects::IssuesController < Projects::ApplicationController class Projects::IssuesController < Projects::ApplicationController
include NotesHelper include RendersNotes
include ToggleSubscriptionAction include ToggleSubscriptionAction
include IssuableActions include IssuableActions
include ToggleAwardEmoji include ToggleAwardEmoji
......
...@@ -3,7 +3,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -3,7 +3,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
include DiffForPath include DiffForPath
include DiffHelper include DiffHelper
include IssuableActions include IssuableActions
include NotesHelper include RendersNotes
include ToggleAwardEmoji include ToggleAwardEmoji
include IssuableCollections include IssuableCollections
......
class Projects::NotesController < Projects::ApplicationController class Projects::NotesController < Projects::ApplicationController
include NotesHelper include RendersNotes
include ToggleAwardEmoji include ToggleAwardEmoji
# Authorize # Authorize
......
class Projects::SnippetsController < Projects::ApplicationController class Projects::SnippetsController < Projects::ApplicationController
include NotesHelper include RendersNotes
include ToggleAwardEmoji include ToggleAwardEmoji
include SpammableActions include SpammableActions
include SnippetsActions include SnippetsActions
......
...@@ -20,9 +20,9 @@ def initialize(project, current_user, params = {}) ...@@ -20,9 +20,9 @@ def initialize(project, current_user, params = {})
end end
def execute def execute
@notes = init_collection notes = init_collection
@notes = since_fetch_at(@params[:last_fetched_at], @notes) if @params[:last_fetched_at] notes = since_fetch_at(notes)
@notes notes.fresh
end end
def target def target
...@@ -56,7 +56,7 @@ def init_collection ...@@ -56,7 +56,7 @@ def init_collection
def notes_of_any_type def notes_of_any_type
types = %w(commit issue merge_request snippet) types = %w(commit issue merge_request snippet)
note_relations = types.map { |t| notes_for_type(t) } note_relations = types.map { |t| notes_for_type(t) }
note_relations.map! { |notes| search(@params[:search], notes) } if @params[:search] note_relations.map! { |notes| search(notes) }
UnionFinder.new.find_union(note_relations, Note) UnionFinder.new.find_union(note_relations, Note)
end end
...@@ -98,16 +98,21 @@ def notes_on_target ...@@ -98,16 +98,21 @@ def notes_on_target
# #
# This method uses ILIKE on PostgreSQL and LIKE on MySQL. # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
# #
def search(query, notes_relation = @notes) def search(query, notes)
query = @params[:search]
return unless query
pattern = "%#{query}%" pattern = "%#{query}%"
notes_relation.where(Note.arel_table[:note].matches(pattern)) notes.where(Note.arel_table[:note].matches(pattern))
end end
# Notes changed since last fetch # Notes changed since last fetch
# Uses overlapping intervals to avoid worrying about race conditions # Uses overlapping intervals to avoid worrying about race conditions
def since_fetch_at(fetch_time, notes_relation = @notes) def since_fetch_at(notes)
return notes unless @params[:last_fetched_at]
# Default to 0 to remain compatible with old clients # Default to 0 to remain compatible with old clients
last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i) last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i)
notes_relation.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh notes.updated_after(last_fetched_at - FETCH_OVERLAP)
end end
end end
...@@ -57,23 +57,6 @@ def link_to_reply_discussion(discussion, line_type = nil) ...@@ -57,23 +57,6 @@ def link_to_reply_discussion(discussion, line_type = nil)
data: data, title: 'Add a reply' data: data, title: 'Add a reply'
end end
def preload_max_access_for_authors(notes, project)
user_ids = notes.map(&:author_id)
project.team.max_member_access_for_user_ids(user_ids)
end
def preload_noteable_for_regular_notes(notes)
ActiveRecord::Associations::Preloader.new.preload(notes.reject(&:for_commit?), :noteable)
end
def prepare_notes_for_rendering(notes)
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
Banzai::NoteRenderer.render(notes, @project, current_user)
notes
end
def note_max_access_for_user(note) def note_max_access_for_user(note)
note.project.team.human_max_access(note.author_id) note.project.team.human_max_access(note.author_id)
end end
......
...@@ -4,7 +4,6 @@ def note_commit_email(recipient_id, note_id) ...@@ -4,7 +4,6 @@ def note_commit_email(recipient_id, note_id)
setup_note_mail(note_id, recipient_id) setup_note_mail(note_id, recipient_id)
@commit = @note.noteable @commit = @note.noteable
@discussion = @note.discussion if @note.part_of_discussion?
@target_url = namespace_project_commit_url(*note_target_url_options) @target_url = namespace_project_commit_url(*note_target_url_options)
mail_answer_thread(@commit, mail_answer_thread(@commit,
...@@ -17,7 +16,6 @@ def note_issue_email(recipient_id, note_id) ...@@ -17,7 +16,6 @@ def note_issue_email(recipient_id, note_id)
setup_note_mail(note_id, recipient_id) setup_note_mail(note_id, recipient_id)
@issue = @note.noteable @issue = @note.noteable
@discussion = @note.discussion if @note.part_of_discussion?
@target_url = namespace_project_issue_url(*note_target_url_options) @target_url = namespace_project_issue_url(*note_target_url_options)
mail_answer_thread(@issue, note_thread_options(recipient_id)) mail_answer_thread(@issue, note_thread_options(recipient_id))
end end
...@@ -26,7 +24,6 @@ def note_merge_request_email(recipient_id, note_id) ...@@ -26,7 +24,6 @@ def note_merge_request_email(recipient_id, note_id)
setup_note_mail(note_id, recipient_id) setup_note_mail(note_id, recipient_id)
@merge_request = @note.noteable @merge_request = @note.noteable
@discussion = @note.discussion if @note.part_of_discussion?
@target_url = namespace_project_merge_request_url(*note_target_url_options) @target_url = namespace_project_merge_request_url(*note_target_url_options)
mail_answer_thread(@merge_request, note_thread_options(recipient_id)) mail_answer_thread(@merge_request, note_thread_options(recipient_id))
end end
...@@ -35,7 +32,6 @@ def note_snippet_email(recipient_id, note_id) ...@@ -35,7 +32,6 @@ def note_snippet_email(recipient_id, note_id)
setup_note_mail(note_id, recipient_id) setup_note_mail(note_id, recipient_id)
@snippet = @note.noteable @snippet = @note.noteable
@discussion = @note.discussion if @note.part_of_discussion?
@target_url = namespace_project_snippet_url(*note_target_url_options) @target_url = namespace_project_snippet_url(*note_target_url_options)
mail_answer_thread(@snippet, note_thread_options(recipient_id)) mail_answer_thread(@snippet, note_thread_options(recipient_id))
end end
...@@ -44,7 +40,6 @@ def note_personal_snippet_email(recipient_id, note_id) ...@@ -44,7 +40,6 @@ def note_personal_snippet_email(recipient_id, note_id)
setup_note_mail(note_id, recipient_id) setup_note_mail(note_id, recipient_id)
@snippet = @note.noteable @snippet = @note.noteable
@discussion = @note.discussion if @note.part_of_discussion?
@target_url = snippet_url(@note.noteable) @target_url = snippet_url(@note.noteable)
mail_answer_thread(@snippet, note_thread_options(recipient_id)) mail_answer_thread(@snippet, note_thread_options(recipient_id))
end end
......
...@@ -2,12 +2,14 @@ module ResolvableDiscussion ...@@ -2,12 +2,14 @@ module ResolvableDiscussion
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
memoized_values << :resolvable memoized_values.push(
memoized_values << :resolved :resolvable,
memoized_values << :first_note :resolved,
memoized_values << :first_note_to_resolve :first_note,
memoized_values << :last_resolved_note :first_note_to_resolve,
memoized_values << :last_note :last_resolved_note,
:last_note
)
delegate :resolved_at, delegate :resolved_at,
:resolved_by, :resolved_by,
......
...@@ -8,7 +8,9 @@ module ResolvableNote ...@@ -8,7 +8,9 @@ module ResolvableNote
validates :resolved_by, presence: true, if: :resolved? validates :resolved_by, presence: true, if: :resolved?
# Keep this scope in sync with the logic in `#potentially_resolvable?` in `Discussion` subclasses that are resolvable # Keep this scope in sync with the logic in `#potentially_resolvable?` in `Discussion` subclasses that are resolvable.
# `RESOLVABLE_TYPES` should include names of all subclasses that are resolvable (where the method can return true), and
# the scope should also match the criteria `ResolvableDiscussion#potentially_resolvable?` puts on resolvability.
scope :potentially_resolvable, -> { where(type: RESOLVABLE_TYPES).where(noteable_type: 'MergeRequest') } scope :potentially_resolvable, -> { where(type: RESOLVABLE_TYPES).where(noteable_type: 'MergeRequest') }
# Keep this scope in sync with `#resolvable?` # Keep this scope in sync with `#resolvable?`
scope :resolvable, -> { potentially_resolvable.user } scope :resolvable, -> { potentially_resolvable.user }
......
# A discussion on merge request or commit diffs consisting of `DiffNote` notes
class DiffDiscussion < Discussion class DiffDiscussion < Discussion
include DiscussionOnDiff include DiscussionOnDiff
......
# A note on merge request or commit diffs
class DiffNote < Note class DiffNote < Note
include NoteOnDiff include NoteOnDiff
......
...@@ -39,6 +39,7 @@ def self.build_discussion_id_base(note) ...@@ -39,6 +39,7 @@ def self.build_discussion_id_base(note)
[:discussion, note.noteable_type.try(:underscore), noteable_id] [:discussion, note.noteable_type.try(:underscore), noteable_id]
end end
# Returns an array of discussion ID components
def self.build_discussion_id(note) def self.build_discussion_id(note)
[*build_discussion_id_base(note), SecureRandom.hex] [*build_discussion_id_base(note), SecureRandom.hex]
end end
......
# A note in a non-diff discussion on an issue, merge request, commit, or snippet
class DiscussionNote < Note class DiscussionNote < Note
NOTEABLE_TYPES = %w(MergeRequest Issue Commit Snippet).freeze NOTEABLE_TYPES = %w(MergeRequest Issue Commit Snippet).freeze
......
# A discussion to wrap a single `Note` note on the root of an issue, merge request,
# commit, or snippet, that is not displayed as a discussion
class IndividualNoteDiscussion < Discussion class IndividualNoteDiscussion < Discussion
# Keep this method in sync with the `potentially_resolvable` scope on `ResolvableNote` # Keep this method in sync with the `potentially_resolvable` scope on `ResolvableNote`
def potentially_resolvable? def potentially_resolvable?
......
# A discussion on merge request or commit diffs consisting of `LegacyDiffNote` notes
class LegacyDiffDiscussion < Discussion class LegacyDiffDiscussion < Discussion
include DiscussionOnDiff include DiscussionOnDiff
......
# A note on merge request or commit diffs, using the legacy implementation
class LegacyDiffNote < Note class LegacyDiffNote < Note
include NoteOnDiff include NoteOnDiff
......
...@@ -68,6 +68,7 @@ class Note < ActiveRecord::Base ...@@ -68,6 +68,7 @@ class Note < ActiveRecord::Base
scope :user, ->{ where(system: false) } scope :user, ->{ where(system: false) }
scope :common, ->{ where(noteable_type: ["", nil]) } scope :common, ->{ where(noteable_type: ["", nil]) }
scope :fresh, ->{ order(created_at: :asc, id: :asc) } scope :fresh, ->{ order(created_at: :asc, id: :asc) }
scope :updated_after, ->(time){ where('updated_at > ?', time) }
scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author_project, ->{ includes(:project, :author) }
scope :inc_author, ->{ includes(:author) } scope :inc_author, ->{ includes(:author) }
scope :inc_relations_for_view, -> do scope :inc_relations_for_view, -> do
...@@ -238,18 +239,20 @@ def discussion_id(noteable = nil) ...@@ -238,18 +239,20 @@ def discussion_id(noteable = nil)
discussion_class(noteable).override_discussion_id(self) || super() discussion_class(noteable).override_discussion_id(self) || super()
end end
# Returns a discussion containing just this note # Returns a discussion containing just this note.
# This method exists as an alternative to `#discussion` to use when the methods
# we intend to call on the Discussion object don't require it to have all of its notes,
# and just depend on the first note or the type of discussion. This saves us a DB query.
def to_discussion(noteable = nil) def to_discussion(noteable = nil)
Discussion.build([self], noteable) Discussion.build([self], noteable)
end end
# Returns the entire discussion this note is part of # Returns the entire discussion this note is part of.
# Consider using `#to_discussion` if we do not need to render the discussion
# and all its notes and if we don't care about the discussion's resolvability status.
def discussion def discussion
if part_of_discussion? full_discussion = self.noteable.notes.find_discussion(self.discussion_id) if part_of_discussion?
self.noteable.notes.find_discussion(self.discussion_id) || to_discussion full_discussion || to_discussion
else
to_discussion
end
end end
def part_of_discussion? def part_of_discussion?
......
# A discussion to wrap a number of `Note` notes on the root of a commit when they
# are displayed in context of a merge request as if they were part of a discussion.
class OutOfContextDiscussion < Discussion class OutOfContextDiscussion < Discussion
# To make sure all out-of-context notes are displayed in one discussion, # To make sure all out-of-context notes are displayed in one discussion,
# we override the discussion ID to be a newly generated but consistent ID. # we override the discussion ID to be a newly generated but consistent ID.
......
# A non-diff discussion on an issue, merge request, commit, or snippet, consisting of `DiscussionNote` notes
class SimpleDiscussion < Discussion class SimpleDiscussion < Discussion
end end
- if @discussion - discussion = @note.discussion unless @discussion.individual_note?
- if discussion
%p.details %p.details
= succeed ':' do = succeed ':' do
= link_to @note.author_name, user_url(@note.author) = link_to @note.author_name, user_url(@note.author)
- if @discussion.diff_discussion? - if discussion.diff_discussion?
- if @discussion.new_discussion? - if discussion.new_discussion?
started a new discussion started a new discussion
- else - else
commented on a discussion commented on a discussion
on #{link_to @discussion.file_path, @target_url} on #{link_to discussion.file_path, @target_url}
- else - else
- if @discussion.new_discussion? - if discussion.new_discussion?
started a new discussion started a new discussion
- else - else
commented on a #{link_to 'discussion', @target_url} commented on a #{link_to 'discussion', @target_url}
...@@ -20,15 +21,15 @@ ...@@ -20,15 +21,15 @@
%p.details %p.details
#{link_to @note.author_name, user_url(@note.author)} commented: #{link_to @note.author_name, user_url(@note.author)} commented:
- if @discussion&.diff_discussion? - if discussion&.diff_discussion?
= content_for :head do = content_for :head do
= stylesheet_link_tag 'mailers/highlighted_diff_email' = stylesheet_link_tag 'mailers/highlighted_diff_email'
%table %table
= render partial: "projects/diffs/line", = render partial: "projects/diffs/line",
collection: @discussion.truncated_diff_lines, collection: discussion.truncated_diff_lines,
as: :line, as: :line,
locals: { diff_file: @discussion.diff_file, locals: { diff_file: discussion.diff_file,
plain: true, plain: true,
email: true } email: true }
......
<% if @discussion -%> <% discussion = @note.discussion unless @discussion.individual_note? -%>
<% if discussion && !discussion.individual_note? -%>
<%= @note.author_name -%> <%= @note.author_name -%>
<% if @discussion.new_discussion? -%> <% if discussion.new_discussion? -%>
<%= " started a new discussion" -%> <%= " started a new discussion" -%>
<% else -%> <% else -%>
<%= " commented on a discussion" -%> <%= " commented on a discussion" -%>
<% end -%> <% end -%>
<% if @discussion.diff_discussion? -%> <% if discussion.diff_discussion? -%>
<%= " on #{@discussion.file_path}" -%> <%= " on #{discussion.file_path}" -%>
<% end -%> <% end -%>
<%= ":" -%> <%= ":" -%>
...@@ -16,8 +17,8 @@ ...@@ -16,8 +17,8 @@
<% end -%> <% end -%>
<% if @discussion&.diff_discussion? -%> <% if discussion&.diff_discussion? -%>
<% @discussion.truncated_diff_lines(highlight: false).each do |line| -%> <% discussion.truncated_diff_lines(highlight: false).each do |line| -%>
<%= "> #{line.text}\n" -%> <%= "> #{line.text}\n" -%>
<% end -%> <% end -%>
......
<%= render partial: 'note_email' %> <%= render 'note_email' %>
<%= render partial: 'note_email' %> <%= render 'note_email' %>
<%= render partial: 'note_email' %> <%= render 'note_email' %>
<%= render partial: 'note_email' %> <%= render 'note_email' %>
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
end end
describe 'GET index' do describe 'GET index' do
let(:last_fetched_at) { '1487756246' }
let(:request_params) do let(:request_params) do
{ {
namespace_id: project.namespace, namespace_id: project.namespace,
...@@ -35,6 +34,8 @@ ...@@ -35,6 +34,8 @@
end end
it 'passes last_fetched_at from headers to NotesFinder' do it 'passes last_fetched_at from headers to NotesFinder' do
last_fetched_at = 3.hours.ago.to_i
request.headers['X-Last-Fetched-At'] = last_fetched_at request.headers['X-Last-Fetched-At'] = last_fetched_at
expect(NotesFinder).to receive(:new) expect(NotesFinder).to receive(:new)
...@@ -47,21 +48,11 @@ ...@@ -47,21 +48,11 @@
context 'for a discussion note' do context 'for a discussion note' do
let!(:note) { create(:discussion_note_on_issue, noteable: issue, project: project) } let!(:note) { create(:discussion_note_on_issue, noteable: issue, project: project) }
it 'includes the ID' do it 'responds with the expected attributes' do
get :index, request_params get :index, request_params
expect(note_json[:id]).to eq(note.id) expect(note_json[:id]).to eq(note.id)
end
it 'includes discussion_html' do
get :index, request_params
expect(note_json[:discussion_html]).not_to be_nil expect(note_json[:discussion_html]).not_to be_nil
end
it "doesn't include diff_discussion_html" do
get :index, request_params
expect(note_json[:diff_discussion_html]).to be_nil expect(note_json[:diff_discussion_html]).to be_nil
end end
end end
...@@ -72,21 +63,11 @@ ...@@ -72,21 +63,11 @@
let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) } let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) }
it 'includes the ID' do it 'responds with the expected attributes' do
get :index, params get :index, params
expect(note_json[:id]).to eq(note.id) expect(note_json[:id]).to eq(note.id)
end
it 'includes discussion_html' do
get :index, params
expect(note_json[:discussion_html]).not_to be_nil