summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDouwe Maan <douwe@selenight.nl>2016-11-27 22:12:02 +0800
committerDouwe Maan <douwe@selenight.nl>2016-11-30 17:46:46 +0800
commit842486f5c812a5a1c7eb585a14f7acbee92ad3a6 (patch)
tree5c64aca547dd08308c9bb755636c16cce60803c1
parent098066050d148deb024fdec6c36bfe9320c674bd (diff)
downloadgitlab-ce-new-resolvable-discussion.tar.gz
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js48
-rw-r--r--app/assets/javascripts/notes.js15
-rw-r--r--app/controllers/projects/discussions_controller.rb2
-rw-r--r--app/controllers/projects/notes_controller.rb41
-rw-r--r--app/finders/notes_finder.rb34
-rw-r--r--app/mailers/emails/issues.rb2
-rw-r--r--app/mailers/emails/merge_requests.rb2
-rw-r--r--app/mailers/notify.rb2
-rw-r--r--app/models/commit_discussion.rb5
-rw-r--r--app/models/concerns/note_on_diff.rb12
-rw-r--r--app/models/concerns/resolvable_note.rb66
-rw-r--r--app/models/diff_discussion.rb61
-rw-r--r--app/models/diff_note.rb59
-rw-r--r--app/models/discussion.rb97
-rw-r--r--app/models/discussion_note.rb27
-rw-r--r--app/models/legacy_diff_discussion.rb13
-rw-r--r--app/models/legacy_diff_note.rb4
-rw-r--r--app/models/merge_request.rb20
-rw-r--r--app/models/note.rb68
-rw-r--r--app/models/sent_notification.rb67
-rw-r--r--app/models/single_note_discussion.rb9
-rw-r--r--app/services/notes/create_service.rb9
-rw-r--r--app/views/discussions/_discussion.html.haml9
-rw-r--r--app/views/discussions/_notes.html.haml5
-rw-r--r--app/views/discussions/_resolve_all.html.haml19
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml2
-rw-r--r--app/views/projects/notes/_form.html.haml9
-rw-r--r--app/views/projects/notes/_notes.html.haml2
-rw-r--r--changelogs/unreleased/new-resolvable-discussion.yml4
-rw-r--r--db/migrate/20161128095517_add_in_reply_to_discussion_id_to_sent_notifications.rb29
-rw-r--r--db/schema.rb1
-rw-r--r--spec/controllers/projects/discussions_controller_spec.rb2
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb10
-rw-r--r--spec/factories/notes.rb8
-rw-r--r--spec/finders/notes_finder_spec.rb58
-rw-r--r--spec/models/concerns/resolvable_note_spec.rb276
-rw-r--r--spec/models/diff_note_spec.rb275
-rw-r--r--spec/models/discussion_note_spec.rb7
-rw-r--r--spec/models/discussion_spec.rb62
-rw-r--r--spec/models/merge_request_spec.rb14
-rw-r--r--spec/models/note_spec.rb47
-rw-r--r--spec/services/notes/create_service_spec.rb3
44 files changed, 932 insertions, 577 deletions
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 4edcaa15fe5..8d422e9366c 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -31,38 +31,64 @@
};
$(document).on('keydown.quick_submit', '.js-quick-submit', function(e) {
- var $form, $submit_button;
+ var $form, $submitButton, $altSubmitButton;
// Enter
if (!keyCodeIs(e, 13)) {
return;
}
- if (!((e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) || (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey))) {
+ if (e.shiftKey || (e.metaKey && e.ctrlKey)) {
+ return;
+ }
+ if (!(e.metaKey || e.ctrlKey)) {
return;
}
- e.preventDefault();
$form = $(e.target).closest('form');
- $submit_button = $form.find('input[type=submit], button[type=submit]');
- if ($submit_button.attr('disabled')) {
+ $submitButton = $form.find('input[type=submit], button[type=submit]');
+ if (e.altKey) {
+ $submitButton = $submitButton.filter('[data-quick-submit-alt]');
+ if ($submitButton.length === 0) {
+ return;
+ }
+ }
+ e.preventDefault();
+ if ($submitButton.attr('disabled')) {
return;
}
- $submit_button.disable();
- return $form.submit();
+ // Click button instead of submitting form, so that button name and value are sent along
+ $submitButton.click();
});
// If the user tabs to a submit button on a `js-quick-submit` form, display a
// tooltip to let them know they could've used the hotkey
$(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function(e) {
- var $this, title;
+ var $this, title, quickSubmitAlt;
// Tab
if (!keyCodeIs(e, 9)) {
return;
}
+
+ $this = $(this);
+
+ quickSubmitAlt = $this.data('quick-submit-alt');
+
if (isMac()) {
- title = "You can also press &#8984;-Enter";
+ if (quickSubmitAlt) {
+ shortcut = "&#8997&#8984;&#9166;";
+ }
+ else {
+ shortcut = "&#8984;&#9166;";
+ }
} else {
- title = "You can also press Ctrl-Enter";
+ if (quickSubmitAlt) {
+ shortcut = "Ctrl-Alt-Enter";
+ }
+ else {
+ shortcut = "Ctrl-Enter";
+ }
}
- $this = $(this);
+
+ title = "You can also press " + shortcut;
+
return $this.tooltip({
container: 'body',
html: 'true',
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 0ca0e255595..96cc6cd6f83 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -194,11 +194,7 @@
_this.last_fetched_at = data.last_fetched_at;
_this.setPollingInterval(data.notes.length);
return $.each(notes, function(i, note) {
- if (note.discussion_html != null) {
- return _this.renderDiscussionNote(note);
- } else {
- return _this.renderNote(note);
- }
+ _this.renderNote(note);
});
};
})(this)
@@ -242,6 +238,10 @@
Notes.prototype.renderNote = function(note) {
var $notesList, votesBlock;
+ if (note.discussion_html != null) {
+ return this.renderDiscussionNote(note);
+ }
+
if (!note.valid) {
if (note.award) {
new Flash('You have already awarded this emoji!', 'alert', this.parentTimeline);
@@ -390,6 +390,7 @@
form.find("#note_line_code").remove();
form.find("#note_position").remove();
form.find("#note_type").remove();
+ form.find("#note_in_reply_to_discussion_id").remove();
form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove();
return this.parentTimeline = form.parents('.timeline');
};
@@ -446,7 +447,7 @@
}
}
- this.renderDiscussionNote(note);
+ this.renderNote(note);
// cleanup after successfully creating a diff/discussion note
this.removeDiscussionNoteForm($form);
};
@@ -641,8 +642,10 @@
form.find("#note_position").val(dataHolder.attr("data-position"));
form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
+ form.find("#note_in_reply_to_discussion_id").val(dataHolder.data("discussionId"));
form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text'));
form.find('.js-note-target-close').remove();
+ form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index d174e1145a7..a8b7267274f 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -30,7 +30,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
end
def discussion
- @discussion ||= @merge_request.find_diff_discussion(params[:id]) || render_404
+ @discussion ||= @merge_request.find_discussion(params[:id]) || render_404
end
def authorize_resolve_discussion!
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 15ca080c696..0661fd20946 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -6,13 +6,14 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
- before_action :find_current_user_notes, only: [:index]
def index
current_fetched_at = Time.now.to_i
notes_json = { notes: [], last_fetched_at: current_fetched_at }
+ @notes = notes_finder.execute
+
@notes.each do |note|
next if note.cross_reference_not_visible_for?(current_user)
@@ -110,6 +111,17 @@ class Projects::NotesController < Projects::ApplicationController
)
end
+ def discussion_html(discussion)
+ return if discussion.single_note?
+
+ render_to_string(
+ "discussions/_discussion",
+ layout: false,
+ formats: [:html],
+ locals: { discussion: discussion }
+ )
+ end
+
def diff_discussion_html(discussion)
return unless discussion.diff_discussion?
@@ -134,17 +146,6 @@ class Projects::NotesController < Projects::ApplicationController
)
end
- def discussion_html(discussion)
- return unless discussion.diff_discussion?
-
- render_to_string(
- "discussions/_discussion",
- layout: false,
- formats: [:html],
- locals: { discussion: discussion }
- )
- end
-
def note_json(note)
attrs = {
award: false,
@@ -167,9 +168,8 @@ class Projects::NotesController < Projects::ApplicationController
note: note.note
)
- if note.diff_note?
- discussion = note.to_discussion
-
+ discussion = note.to_discussion
+ unless discussion.single_note?
attrs.merge!(
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
@@ -212,11 +212,16 @@ class Projects::NotesController < Projects::ApplicationController
def note_params
params.require(:note).permit(
:note, :noteable, :noteable_id, :noteable_type, :project_id,
- :attachment, :line_code, :commit_id, :type, :position
+ :attachment, :line_code, :commit_id, :type, :position,
+ :in_reply_to_discussion_id, :new_discussion
)
end
- def find_current_user_notes
- @notes = NotesFinder.new.execute(project, current_user, params)
+ def notes_finder
+ @notes_finder ||= NotesFinder.new(project, current_user, params)
+ end
+
+ def target
+ notes_finder.target
end
end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index a653a6d59c6..fc86acd9329 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -1,27 +1,41 @@
class NotesFinder
FETCH_OVERLAP = 5.seconds
- def execute(project, current_user, params)
+ attr_accessor :project, :current_user, :params
+
+ def initialize(project, current_user, params)
+ @project, @current_user, @params = project, current_user, params
+ end
+
+ def execute
+ notes =
+ if target.respond_to?(:related_notes)
+ target.related_notes
+ else
+ target.notes
+ end
+
+ last_fetched_at = Time.at(params.fetch(:last_fetched_at, 0).to_i)
+ # Use overlapping intervals to avoid worrying about race conditions
+ notes.inc_author.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
+ end
+
+ def target
target_type = params[:target_type]
target_id = params[:target_id]
- # Default to 0 to remain compatible with old clients
- last_fetched_at = Time.at(params.fetch(:last_fetched_at, 0).to_i)
notes =
case target_type
when "commit"
- project.notes.for_commit_id(target_id).non_diff_notes
+ project.commit(target_id)
when "issue"
- IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author
+ IssuesFinder.new(current_user, project_id: project.id).find(target_id)
when "merge_request"
- project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
+ project.merge_requests.find(target_id)
when "snippet", "project_snippet"
- project.snippets.find(target_id).notes
+ project.snippets.find(target_id)
else
raise 'invalid target_type'
end
-
- # Use overlapping intervals to avoid worrying about race conditions
- notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
end
end
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index d64e48f774b..dea282de4b3 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -3,7 +3,7 @@ module Emails
def new_issue_email(recipient_id, issue_id)
setup_issue_mail(issue_id, recipient_id)
- mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
+ mail_new_discussion(@issue, issue_thread_options(@issue.author_id, recipient_id))
end
def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id)
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index ec27ac517db..2a6b0371adb 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -3,7 +3,7 @@ module Emails
def new_merge_request_email(recipient_id, merge_request_id)
setup_merge_request_mail(merge_request_id, recipient_id)
- mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id))
+ mail_new_discussion(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id))
end
def new_mention_in_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 0bc1c19e9cd..589ac2a9885 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -136,7 +136,7 @@ class Notify < BaseMailer
# with headers suitable for grouping by thread in email clients.
#
# See: mail_answer_thread
- def mail_new_thread(model, headers = {})
+ def mail_new_discussion(model, headers = {})
headers['Message-ID'] = message_id(model)
mail_thread(model, headers)
diff --git a/app/models/commit_discussion.rb b/app/models/commit_discussion.rb
new file mode 100644
index 00000000000..c6e026334bb
--- /dev/null
+++ b/app/models/commit_discussion.rb
@@ -0,0 +1,5 @@
+class CommitDiscussion < Discussion
+ def potentially_resolvable?
+ false
+ end
+end
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
index b8dd27a7afe..abf8ba1b1d8 100644
--- a/app/models/concerns/note_on_diff.rb
+++ b/app/models/concerns/note_on_diff.rb
@@ -5,6 +5,10 @@ module NoteOnDiff
true
end
+ def part_of_discussion?
+ true
+ end
+
def diff_file
raise NotImplementedError
end
@@ -24,12 +28,4 @@ module NoteOnDiff
def diff_attributes
raise NotImplementedError
end
-
- def can_be_award_emoji?
- false
- end
-
- def to_discussion
- Discussion.new([self])
- end
end
diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb
new file mode 100644
index 00000000000..d34d6e2b203
--- /dev/null
+++ b/app/models/concerns/resolvable_note.rb
@@ -0,0 +1,66 @@
+module ResolvableNote
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :resolved_by, class_name: "User"
+
+ validates :resolved_by, presence: true, if: :resolved?
+
+ # Keep this scope in sync with the logic in `#resolvable?` in `Note` subclasses that are resolvable
+ scope :resolvable, -> { where(type: ['DiffNote', 'DiscussionNote']).user.where(noteable_type: 'MergeRequest') }
+ scope :resolved, -> { resolvable.where.not(resolved_at: nil) }
+ scope :unresolved, -> { resolvable.where(resolved_at: nil) }
+ end
+
+ module ClassMethods
+ # To be defined in any Note subclasses that are actually resolvable
+ # def resolvable?
+ # true
+ # end
+
+ # This method must be kept in sync with `#resolve!`
+ def resolve!(current_user)
+ unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id)
+ end
+
+ # This method must be kept in sync with `#unresolve!`
+ def unresolve!
+ resolved.update_all(resolved_at: nil, resolved_by_id: nil)
+ end
+ end
+
+ # If you update this method remember to also update the scope `resolvable`
+ def resolvable?
+ self.class.resolvable? && !system?
+ end
+
+ def resolved?
+ return false unless resolvable?
+
+ self.resolved_at.present?
+ end
+
+ def to_be_resolved?
+ resolvable? && !resolved?
+ end
+
+ # If you update this method remember to also update `.resolve!`
+ def resolve!(current_user)
+ return unless resolvable?
+ return if resolved?
+
+ self.resolved_at = Time.now
+ self.resolved_by = current_user
+ save!
+ end
+
+ # If you update this method remember to also update `.unresolve!`
+ def unresolve!
+ return unless resolvable?
+ return unless resolved?
+
+ self.resolved_at = nil
+ self.resolved_by = nil
+ save!
+ end
+end
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
new file mode 100644
index 00000000000..1b24b5d1faa
--- /dev/null
+++ b/app/models/diff_discussion.rb
@@ -0,0 +1,61 @@
+class DiffDiscussion < Discussion
+ NUMBER_OF_TRUNCATED_DIFF_LINES = 16
+
+ delegate :line_code,
+ :original_line_code,
+ :diff_file,
+ :for_line?,
+ :active?,
+
+ to: :first_note
+
+ delegate :blob,
+ :highlighted_diff_lines,
+ :diff_lines,
+
+ to: :diff_file,
+ allow_nil: true
+
+ def diff_discussion?
+ true
+ end
+
+ def legacy_diff_discussion?
+ false
+ end
+
+ def potentially_resolvable?
+ first_note.for_merge_request?
+ end
+
+ def active?
+ return @active if @active.present?
+
+ @active = first_note.active?
+ end
+ MEMOIZED_VALUES << :active
+
+ def reply_attributes
+ super.merge(first_note.diff_attributes)
+ end
+
+ # Returns an array of at most 16 highlighted lines above a diff note
+ def truncated_diff_lines(highlight: true)
+ lines = highlight ? highlighted_diff_lines : diff_lines
+ prev_lines = []
+
+ lines.each do |line|
+ if line.meta?
+ prev_lines.clear
+ else
+ prev_lines << line
+
+ break if for_line?(line)
+
+ prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES
+ end
+ end
+
+ prev_lines
+ end
+end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 559b3075905..829c8516a5d 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -9,15 +9,9 @@ class DiffNote < Note
validates :diff_line, presence: true
validates :line_code, presence: true, line_code: true
validates :noteable_type, inclusion: { in: ['Commit', 'MergeRequest'] }
- validates :resolved_by, presence: true, if: :resolved?
validate :positions_complete
validate :verify_supported
- # Keep this scope in sync with the logic in `#resolvable?`
- scope :resolvable, -> { user.where(noteable_type: 'MergeRequest') }
- scope :resolved, -> { resolvable.where.not(resolved_at: nil) }
- scope :unresolved, -> { resolvable.where(resolved_at: nil) }
-
after_initialize :ensure_original_discussion_id
before_validation :set_original_position, :update_position, on: :create
before_validation :set_line_code, :set_original_discussion_id
@@ -27,18 +21,12 @@ class DiffNote < Note
after_save :keep_around_commits
class << self
- def build_discussion_id(noteable_type, noteable_id, position)
- [super(noteable_type, noteable_id), *position.key].join("-")
+ def resolvable?
+ true
end
- # This method must be kept in sync with `#resolve!`
- def resolve!(current_user)
- unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id)
- end
-
- # This method must be kept in sync with `#unresolve!`
- def unresolve!
- resolved.update_all(resolved_at: nil, resolved_by_id: nil)
+ def build_discussion_id(noteable_type, noteable_id, position)
+ [super(noteable_type, noteable_id), *position.key].join("-")
end
end
@@ -46,6 +34,10 @@ class DiffNote < Note
true
end
+ def discussion_class
+ DiffDiscussion
+ end
+
def diff_attributes
{ position: position.to_json }
end
@@ -88,41 +80,8 @@ class DiffNote < Note
self.position.diff_refs == diff_refs
end
- # If you update this method remember to also update the scope `resolvable`
def resolvable?
- !system? && for_merge_request?
- end
-
- def resolved?
- return false unless resolvable?
-
- self.resolved_at.present?
- end
-
- # If you update this method remember to also update `.resolve!`
- def resolve!(current_user)
- return unless resolvable?
- return if resolved?
-
- self.resolved_at = Time.now
- self.resolved_by = current_user
- save!
- end
-
- # If you update this method remember to also update `.unresolve!`
- def unresolve!
- return unless resolvable?
- return unless resolved?
-
- self.resolved_at = nil
- self.resolved_by = nil
- save!
- end
-
- def discussion
- return unless resolvable?
-
- self.noteable.find_diff_discussion(self.discussion_id)
+ super && for_merge_request?
end
private
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 75a85563235..ceadaebe996 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -1,5 +1,5 @@
class Discussion
- NUMBER_OF_TRUNCATED_DIFF_LINES = 16
+ MEMOIZED_VALUES = []
attr_reader :notes
@@ -11,12 +11,6 @@ class Discussion
:for_commit?,
:for_merge_request?,
- :line_code,
- :original_line_code,
- :diff_file,
- :for_line?,
- :active?,
-
to: :first_note
delegate :resolved_at,
@@ -25,19 +19,12 @@ class Discussion
to: :last_resolved_note,
allow_nil: true
- delegate :blob,
- :highlighted_diff_lines,
- :diff_lines,
-
- to: :diff_file,
- allow_nil: true
-
- def self.for_notes(notes)
- notes.group_by(&:discussion_id).values.map { |notes| new(notes) }
+ def self.build(notes)
+ notes.first.discussion_class.new(notes)
end
- def self.for_diff_notes(notes)
- notes.group_by(&:line_code).values.map { |notes| new(notes) }
+ def self.build_collection(notes)
+ notes.group_by(&:discussion_id).values.map { |notes| build(notes) }
end
def initialize(notes)
@@ -49,6 +36,7 @@ class Discussion
@last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last
end
+ MEMOIZED_VALUES << :last_resolved_note
def last_updated_at
last_note.created_at
@@ -65,32 +53,40 @@ class Discussion
alias_method :to_param, :id
def diff_discussion?
- first_note.diff_note?
+ false
end
- def legacy_diff_discussion?
- notes.any?(&:legacy_diff_note?)
+ def potentially_resolvable?
+ true
+ end
+
+ def single_note?
+ false
end
def resolvable?
return @resolvable if @resolvable.present?
- @resolvable = diff_discussion? && notes.any?(&:resolvable?)
+ @resolvable = potentially_resolvable? && notes.any?(&:resolvable?)
end
+ MEMOIZED_VALUES << :resolvable
def resolved?
return @resolved if @resolved.present?
@resolved = resolvable? && notes.none?(&:to_be_resolved?)
end
+ MEMOIZED_VALUES << :resolved
def first_note
@first_note ||= @notes.first
end
+ MEMOIZED_VALUES << :first_note
def last_note
@last_note ||= @notes.last
end
+ MEMOIZED_VALUES << :last_note
def resolved_notes
notes.select(&:resolved?)
@@ -120,25 +116,12 @@ class Discussion
update { |notes| notes.unresolve! }
end
- def for_target?(target)
- self.noteable == target && !diff_discussion?
- end
-
- def active?
- return @active if @active.present?
-
- @active = first_note.active?
- end
-
def collapsed?
- return false unless diff_discussion?
-
if resolvable?
# New diff discussions only disappear once they are marked resolved
resolved?
else
- # Old diff discussions disappear once they become outdated
- !active?
+ false
end
end
@@ -147,52 +130,28 @@ class Discussion
end
def reply_attributes
- data = {
+ {
noteable_type: first_note.noteable_type,
noteable_id: first_note.noteable_id,
commit_id: first_note.commit_id,
discussion_id: self.id,
+ note_type: first_note.type
}
-
- if diff_discussion?
- data[:note_type] = first_note.type
-
- data.merge!(first_note.diff_attributes)
- end
-
- data
- end
-
- # Returns an array of at most 16 highlighted lines above a diff note
- def truncated_diff_lines(highlight: true)
- lines = highlight ? highlighted_diff_lines : diff_lines
- prev_lines = []
-
- lines.each do |line|
- if line.meta?
- prev_lines.clear
- else
- prev_lines << line
-
- break if for_line?(line)
-
- prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES
- end
- end
-
- prev_lines
end
private
def update
- notes_relation = DiffNote.where(id: notes.map(&:id)).fresh
+ # Do not select `Note.resolvable`, so that system notes remain in the collection
+ notes_relation = Note.where(id: notes.map(&:id))
+
yield(notes_relation)
# Set the notes array to the updated notes
- @notes = notes_relation.to_a
+ @notes = notes_relation.fresh.to_a
- # Reset the memoized values
- @last_resolved_note = @resolvable = @resolved = @first_note = @last_note = nil
+ MEMOIZED_VALUES.each do |var|
+ instance_variable_set(:"@#{var}", nil)
+ end
end
end
diff --git a/app/models/discussion_note.rb b/app/models/discussion_note.rb
new file mode 100644
index 00000000000..b1b4905a512
--- /dev/null
+++ b/app/models/discussion_note.rb
@@ -0,0 +1,27 @@
+class DiscussionNote < Note
+ validates :noteable_type, inclusion: { in: ['MergeRequest'] }
+
+ class << self
+ def resolvable?
+ true
+ end
+ end
+
+ def part_of_discussion?
+ true
+ end
+
+ def discussion_class
+ Discussion
+ end
+
+ private
+
+ def set_discussion_id
+ if in_reply_to_discussion_id
+ self.discussion_id = in_reply_to_discussion_id
+ else
+ super
+ end
+ end
+end
diff --git a/app/models/legacy_diff_discussion.rb b/app/models/legacy_diff_discussion.rb
new file mode 100644
index 00000000000..59bc68da052
--- /dev/null
+++ b/app/models/legacy_diff_discussion.rb
@@ -0,0 +1,13 @@
+class LegacyDiffDiscussion < DiffDiscussion
+ def legacy_diff_discussion?
+ true
+ end
+
+ def potentially_resolvable?
+ false
+ end
+
+ def collapsed?
+ !active?
+ end
+end
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index 40277a9b139..b71aa71b8fe 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -13,6 +13,10 @@ class LegacyDiffNote < Note
end
end
+ def discussion_class
+ LegacyDiffDiscussion
+ end
+
def legacy_diff_note?
true
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 38d8c15e6b0..4b64894f182 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -453,7 +453,7 @@ class MergeRequest < ActiveRecord::Base
should_remove_source_branch? || force_remove_source_branch?
end
- def mr_and_commit_notes
+ def related_notes
# Fetch comments only from last 100 commits
commits_for_notes_limit = 100
commit_ids = commits.last(commits_for_notes_limit).map(&:id)
@@ -469,29 +469,25 @@ class MergeRequest < ActiveRecord::Base
end
def discussions
- @discussions ||= self.mr_and_commit_notes.
+ @discussions ||= self.related_notes.
inc_relations_for_view.
- fresh.
discussions
end
- def diff_discussions
- @diff_discussions ||= self.notes.diff_notes.discussions
+ def find_discussion(discussion_id)
+ self.related_notes.find_discussion(discussion_id)
end
- def find_diff_discussion(discussion_id)
- notes = self.notes.diff_notes.where(discussion_id: discussion_id).fresh.to_a
- return if notes.empty?
-
- Discussion.new(notes)
+ def resolvable_discussions
+ @resolvable_discussions ||= self.notes.resolvable.discussions
end
def discussions_resolvable?
- diff_discussions.any?(&:resolvable?)
+ resolvable_discussions.any?(&:resolvable?)
end
def discussions_resolved?
- discussions_resolvable? && diff_discussions.none?(&:to_be_resolved?)
+ discussions_resolvable? && resolvable_discussions.none?(&:to_be_resolved?)
end
def discussions_to_be_resolved?
diff --git a/app/models/note.rb b/app/models/note.rb
index 5b50ca285c3..3e8aa49a601 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -8,6 +8,7 @@ class Note < ActiveRecord::Base
include FasterCacheKeys
include CacheMarkdownField
include AfterCommitQueue
+ include ResolvableNote
cache_markdown_field :note, pipeline: :note
@@ -22,6 +23,9 @@ class Note < ActiveRecord::Base
# Attribute used to store the attributes that have ben changed by slash commands.
attr_accessor :commands_changes
+ # The discussion ID for the `DiscussionNote` thread being replied to
+ attr_accessor :in_reply_to_discussion_id
+
default_value_for :system, false
attr_mentionable :note, pipeline: :note
@@ -32,9 +36,6 @@ class Note < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :updated_by, class_name: "User"
- # Only used by DiffNote, but defined here so that it can be used in `Note.includes`
- belongs_to :resolved_by, class_name: "User"
-
has_many :todos, dependent: :destroy
has_many :events, as: :target, dependent: :destroy
@@ -52,6 +53,7 @@ class Note < ActiveRecord::Base
validates :noteable_id, presence: true, unless: [:for_commit?, :importing?]
validates :commit_id, presence: true, if: :for_commit?
validates :author, presence: true
+ validates :discussion_id, presence: true, format: { with: /\A\h{40}\z/ }
validate unless: [:for_commit?, :importing?] do |note|
unless note.noteable.try(:project) == note.project
@@ -71,8 +73,9 @@ class Note < ActiveRecord::Base
scope :inc_author, ->{ includes(:author) }
scope :inc_relations_for_view, ->{ includes(:project, :author, :updated_by, :resolved_by, :award_emoji) }
+ scope :discussion_notes, ->{ where(type: 'DiscussionNote') }
scope :diff_notes, ->{ where(type: ['LegacyDiffNote', 'DiffNote']) }
- scope :non_diff_notes, ->{ where(type: ['Note', nil]) }
+ scope :non_diff_notes, ->{ where(type: ['Note', 'DiscussionNote', nil]) }
scope :with_associations, -> do
# FYI noteable cannot be loaded for LegacyDiffNote for commits
@@ -98,14 +101,28 @@ class Note < ActiveRecord::Base
Digest::SHA1.hexdigest(build_discussion_id(*args))
end
+ def resolvable?
+ false
+ end
+
def discussions
- Discussion.for_notes(all)
+ Discussion.build_collection(all.fresh)
+ end
+
+ def find_discussion(discussion_id)
+ notes = where(discussion_id: discussion_id).fresh.to_a
+ return if notes.empty?
+
+ Discussion.build(notes)
end
def grouped_diff_discussions
- active_notes = diff_notes.fresh.select(&:active?)
- Discussion.for_diff_notes(active_notes).
- map { |d| [d.line_code, d] }.to_h
+ diff_notes.
+ fresh.
+ select(&:active?).
+ group_by(&:line_code).
+ map { |line_code, notes| [line_code, DiffDiscussion.build(notes)] }.
+ to_h
end
# Searches for notes matching the given query.
@@ -146,18 +163,10 @@ class Note < ActiveRecord::Base
true
end
- def resolvable?
- false
- end
-
- def resolved?
+ def part_of_discussion?
false
end
- def to_be_resolved?
- resolvable? && !resolved?
- end
-
def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i
end
@@ -226,7 +235,7 @@ class Note < ActiveRecord::Base
end
def can_be_award_emoji?
- noteable.is_a?(Awardable)
+ noteable.is_a?(Awardable) && !part_of_discussion?
end
def contains_emoji_only?
@@ -237,6 +246,29 @@ class Note < ActiveRecord::Base
note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
end
+ def discussion_class
+ if for_merge_request?
+ # Notes on merge requests are always in a discussion of their own
+ SingleNoteDiscussion
+ else
+ CommitDiscussion
+ end
+ end
+
+ # Returns a discussion containing just this note
+ def to_discussion
+ Discussion.build([self])
+ end
+
+ # Returns the entire discussion this note is part of
+ def discussion
+ if part_of_discussion?
+ self.noteable.notes.find_discussion(self.discussion_id)
+ else
+ to_discussion
+ end
+ end
+
private
def keep_around_commit
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index f4bcb49b34d..558e4c1a7eb 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -5,10 +5,11 @@ class SentNotification < ActiveRecord::Base
belongs_to :noteable, polymorphic: true
belongs_to :recipient, class_name: "User"
- validates :project, :recipient, :reply_key, presence: true
- validates :reply_key, uniqueness: true
+ validates :project, :recipient, presence: true
+ validates :reply_key, presence: true, uniqueness: true
validates :noteable_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
+ validates :in_reply_to_discussion_id, format: { with: /\A\h{40}\z/, if: :discussion_note? }
validate :note_valid
after_save :keep_around_commit
@@ -34,23 +35,24 @@ class SentNotification < ActiveRecord::Base
end
attrs.reverse_merge!(
- project: noteable.project,
- noteable_type: noteable.class.name,
- noteable_id: noteable_id,
- commit_id: commit_id,
- recipient_id: recipient_id,
- reply_key: reply_key
+ project: noteable.project,
+ noteable_type: noteable.class.name,
+ noteable_id: noteable_id,
+ commit_id: commit_id,
+ recipient_id: recipient_id,
+ reply_key: reply_key
)
create(attrs)
end
def record_note(note, recipient_id, reply_key, attrs = {})
- if note.diff_note?
- attrs[:note_type] = note.type
+ attrs.merge!(
+ note_type: note.type,
+ in_reply_to_discussion_id: note.discussion_id
+ )
- attrs.merge!(note.diff_attributes)
- end
+ attrs.merge!(note.diff_attributes) if note.diff_note?
record(note.noteable, recipient_id, reply_key, attrs)
end
@@ -64,6 +66,10 @@ class SentNotification < ActiveRecord::Base
noteable_type == "Commit"
end
+ def discussion_note?
+ note_type == DiscussionNote.name
+ end
+
def noteable
if for_commit?
project.commit(commit_id) rescue nil
@@ -89,31 +95,38 @@ class SentNotification < ActiveRecord::Base
self.reply_key
end
- def note_attributes
- {
- project: self.project,
- author: self.recipient,
- type: self.note_type,
- noteable_type: self.noteable_type,
- noteable_id: self.noteable_id,
- commit_id: self.commit_id,
- line_code: self.line_code,
- position: self.position.to_json
- }
- end
-
def create_note(note)
Notes::CreateService.new(
self.project,
self.recipient,
- self.note_attributes.merge(note: note)
+ self.note_params.merge(note: note)
).execute
end
private
+ def note_params
+ {
+ project: self.project,
+ author: self.recipient,
+ type: self.note_type,
+ noteable_type: self.noteable_type,
+ noteable_id: self.noteable_id,
+ commit_id: self.commit_id,
+
+ # LegacyDiffNote
+ line_code: self.line_code,
+
+ # DiffNote
+ position: self.position.to_json,
+
+ # DiscussionNote
+ in_reply_to_discussion_id: self.in_reply_to_discussion_id
+ }
+ end
+
def note_valid
- Note.new(note_attributes.merge(note: "Test")).valid?
+ Note.new(note_params.merge(note: "Test")).valid?
end
def keep_around_commit
diff --git a/app/models/single_note_discussion.rb b/app/models/single_note_discussion.rb
new file mode 100644
index 00000000000..ee1aecb6b96
--- /dev/null
+++ b/app/models/single_note_discussion.rb
@@ -0,0 +1,9 @@
+class SingleNoteDiscussion < Discussion
+ def potentially_resolvable?
+ false
+ end
+
+ def single_note?
+ true
+ end
+end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index d75592e31f3..82bde6f38bc 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -1,10 +1,19 @@
module Notes
class CreateService < BaseService
def execute
+ in_reply_to_discussion_id = params.delete(:in_reply_to_discussion_id)
+
+ params[:type] = DiscussionNote.name if params.delete(:new_discussion)
+
note = project.notes.new(params)
note.author = current_user
note.system = false
+ if note.is_a?(DiscussionNote) && in_reply_to_discussion_id
+ discussion_exists = note.noteable.notes.discussion_notes.find_discussion(in_reply_to_discussion_id)
+ note.in_reply_to_discussion_id = in_reply_to_discussion_id if discussion_exists
+ end
+
if note.award_emoji?
noteable = note.noteable
if noteable.user_can_award?(current_user, note.award_emoji_name)
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 2bce2780484..1a430f9810b 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -15,16 +15,19 @@
.inline.discussion-headline-light
= discussion.author.to_reference
- started a discussion on
+ started a discussion
- if discussion.for_commit?
+ on
- commit = discussion.noteable
- if commit
commit
- = link_to commit.short_id, namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code), class: 'monospace'
+ - anchor = discussion.line_code if discussion.diff_discussion?
+ = link_to commit.short_id, namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: anchor), class: 'monospace'
- else
a deleted commit
- - else
+ - elsif discussion.diff_discussion?
+ on
- if discussion.active?
= link_to diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code) do
the diff
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index dfdbdf1f969..819bf553f00 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -3,14 +3,13 @@
- if current_user
.discussion-reply-holder
- - if discussion.diff_discussion?
+ - if discussion.potentially_resolvable?
- line_type = local_assigns.fetch(:line_type, nil)
.btn-group-justified.discussion-with-resolve-btn{ role: "group" }
.btn-group{ role: "group" }
= link_to_reply_discussion(discussion, line_type)
= render "discussions/resolve_all", discussion: discussion
- - if discussion.for_merge_request?
- = render "discussions/jump_to_next", discussion: discussion
+ = render "discussions/jump_to_next", discussion: discussion
- else
= link_to_reply_discussion(discussion)
diff --git a/app/views/discussions/_resolve_all.html.haml b/app/views/discussions/_resolve_all.html.haml
index f0b61e0f7de..ebdc341d748 100644
--- a/app/views/discussions/_resolve_all.html.haml
+++ b/app/views/discussions/_resolve_all.html.haml
@@ -1,10 +1,9 @@
-- if discussion.for_merge_request?
- %resolve-discussion-btn{ ":project-path" => "'#{project_path(discussion.project)}'",
- ":discussion-id" => "'#{discussion.id}'",
- ":merge-request-id" => discussion.noteable.iid,
- ":can-resolve" => discussion.can_resolve?(current_user),
- "inline-template" => true }
- .btn-group{ role: "group", "v-if" => "showButton" }
- %button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading", "v-cloak" => "true" }
- = icon("spinner spin", "v-show" => "loading")
- {{ buttonText }}
+%resolve-discussion-btn{ ":project-path" => "'#{project_path(discussion.project)}'",
+ ":discussion-id" => "'#{discussion.id}'",
+ ":merge-request-id" => discussion.noteable.iid,
+ ":can-resolve" => discussion.can_resolve?(current_user),
+ "inline-template" => true }
+ .btn-group{ role: "group", "v-if" => "showButton" }
+ %button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading", "v-cloak" => "true" }
+ = icon("spinner spin", "v-show" => "loading")
+ {{ buttonText }}
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index cfb44bd206c..1f1d698202c 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -1,4 +1,6 @@
- content_for :note_actions do
+ = submit_tag 'Start new discussion', name: 'note[new_discussion]', class: "btn btn-nr append-right-10 comment-btn js-comment-button btn-inverted js-note-new-discussion", data: { quick_submit_alt: true }
+
- if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open?
= link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"}
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 9ffcc48eb80..1cde318bbae 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -39,7 +39,7 @@
= icon('thumbs-down')
= downvotes
- - note_count = merge_request.mr_and_commit_notes.user.count
+ - note_count = merge_request.related_notes.user.count
%li
= link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
= icon('comments')
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 0e2975bd551..306c754215a 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -53,7 +53,7 @@
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
Discussion
- %span.badge= @merge_request.mr_and_commit_notes.user.count
+ %span.badge= @merge_request.related_notes.user.count
- if @merge_request.source_project
%li.commits-tab
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 46b402545cd..8ac70cafd80 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -4,12 +4,13 @@
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
= note_target_fields(@note)
- = f.hidden_field :commit_id
- = f.hidden_field :line_code
- = f.hidden_field :noteable_id
- = f.hidden_field :noteable_type
= f.hidden_field :type
+ = f.hidden_field :noteable_type
+ = f.hidden_field :noteable_id
+ = f.hidden_field :commit_id
= f.hidden_field :position
+ = f.hidden_field :line_code
+ = f.hidden_field :in_reply_to_discussion_id
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f,
diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/projects/notes/_notes.html.haml
index 022578bd6db..5232b1d07cd 100644
--- a/app/views/projects/notes/_notes.html.haml
+++ b/app/views/projects/notes/_notes.html.haml
@@ -1,6 +1,6 @@
- if @discussions.present?
- @discussions.each do |discussion|
- - if discussion.for_target?(@noteable)
+ - if discussion.single_note?
= render partial: "projects/notes/note", object: discussion.first_note, as: :note
- else
= render 'discussions/discussion', discussion: discussion
diff --git a/changelogs/unreleased/new-resolvable-discussion.yml b/changelogs/unreleased/new-resolvable-discussion.yml
new file mode 100644
index 00000000000..af1de6a45e7
--- /dev/null
+++ b/changelogs/unreleased/new-resolvable-discussion.yml
@@ -0,0 +1,4 @@
+---
+title: Add option to start a new resolvable discussion in an MR
+merge_request:
+author:
diff --git a/db/migrate/20161128095517_add_in_reply_to_discussion_id_to_sent_notifications.rb b/db/migrate/20161128095517_add_in_reply_to_discussion_id_to_sent_notifications.rb
new file mode 100644
index 00000000000..d56d83ca1d3
--- /dev/null
+++ b/db/migrate/20161128095517_add_in_reply_to_discussion_id_to_sent_notifications.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddInReplyToDiscussionIdToSentNotifications < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :sent_notifications, :in_reply_to_discussion_id, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 0d510c8a269..dc27c7450e9 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1001,6 +1001,7 @@ ActiveRecord::Schema.define(version: 20161128161412) do
t.string "line_code"
t.string "note_type"
t.text "position"
+ t.string "in_reply_to_discussion_id"
end
add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb
index ff617fea847..e91832459b6 100644
--- a/spec/controllers/projects/discussions_controller_spec.rb
+++ b/spec/controllers/projects/discussions_controller_spec.rb
@@ -4,7 +4,7 @@ describe Projects::DiscussionsController do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project) }
- let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+ let(:note) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
let(:discussion) { note.discussion }
let(:request_params) do
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 92e38b02615..f6de25b7243 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -14,6 +14,16 @@ describe Projects::NotesController do
}
end
+ describe 'GET index' do
+ # It renders the discussion partial for any threaded note
+ # TODO: Test
+ end
+
+ describe 'POST create' do
+ # Test :type, :new_discussion, :in_reply_to_discussion_id (in_reply_to_id?)
+ # TODO: Test
+ end
+
describe 'POST toggle_award_emoji' do
before do
sign_in(user)
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 6919002dedc..2572eabe6be 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -15,6 +15,14 @@ FactoryGirl.define do
factory :note_on_project_snippet, traits: [:on_project_snippet]
factory :system_note, traits: [:system]
+
+ factory :discussion_note_on_merge_request, traits: [:on_merge_request], class: DiscussionNote do
+ trait :resolved do
+ resolved_at { Time.now }
+ resolved_by { create(:user) }
+ end
+ end
+
factory :legacy_diff_note_on_commit, traits: [:on_commit, :legacy_diff_note], class: LegacyDiffNote
factory :legacy_diff_note_on_merge_request, traits: [:on_merge_request, :legacy_diff_note], class: LegacyDiffNote
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 7c6860372cc..0e1d98066dd 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -3,58 +3,74 @@ require 'spec_helper'
describe NotesFinder do
let(:user) { create :user }
let(:project) { create :project }
- let(:note1) { create :note_on_commit, project: project }
- let(:note2) { create :note_on_commit, project: project }
+ let!(:note1) { create :note_on_commit, project: project }
+ let!(:note2) { create :note_on_commit, project: project }
let(:commit) { note1.noteable }
+ subject { NotesFinder.new(project, user, params) }
before do
- project.team << [user, :master]
+ # project.team << [user, :master]
end
describe '#execute' do
let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } }
- before do
- note1
- note2
- end
-
it 'finds all notes' do
- notes = NotesFinder.new.execute(project, user, params)
+ notes = subject.execute
expect(notes.size).to eq(2)
end
it 'raises an exception for an invalid target_type' do
params.merge!(target_type: 'invalid')
- expect { NotesFinder.new.execute(project, user, params) }.to raise_error('invalid target_type')
+ expect { subject.execute }.to raise_error('invalid target_type')
end
it 'filters out old notes' do
note2.update_attribute(:updated_at, 2.hours.ago)
- notes = NotesFinder.new.execute(project, user, params)
+ notes = subject.execute
expect(notes).to eq([note1])
end
context 'confidential issue notes' do
- let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project) }
let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) }
let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } }
- it 'returns notes if user can see the issue' do
- expect(NotesFinder.new.execute(project, user, params)).to eq([confidential_note])
+ context 'when user is the author of the issue' do
+ before do
+ confidential_issue.update(author: user)
+ end
+
+ it 'returns confidential notes' do
+ expect(subject.execute).to eq([confidential_note])
+ end
end
- it 'raises an error if user can not see the issue' do
- user = create(:user)
- expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
+ context 'when user is a master on the team' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'returns confidential notes' do
+ expect(subject.execute).to eq([confidential_note])
+ end
end
- it 'raises an error for project members with guest role' do
- user = create(:user)
- project.team << [user, :guest]
+ context 'when user is a guest on the team' do
+ before do
+ project.team << [user, :guest]
+ end
+
+ it 'raises an error' do
+ expect { subject.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
- expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
+ context 'when user is not a member of the team' do
+ it 'raises an error if user can not see the issue' do
+ expect { subject.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
end
end
end
diff --git a/spec/models/concerns/resolvable_note_spec.rb b/spec/models/concerns/resolvable_note_spec.rb
new file mode 100644
index 00000000000..f71704e6538
--- /dev/null
+++ b/spec/models/concerns/resolvable_note_spec.rb
@@ -0,0 +1,276 @@
+require 'spec_helper'
+
+describe Note, ResolvableNote, models: true do
+ subject { create(:discussion_note_on_merge_request) }
+
+ describe '.resolvable' do
+ # TODO: Test
+ end
+
+ describe '.resolved' do
+ # TODO: Test
+ end
+
+ describe '.unresolved' do
+ # TODO: Test
+ end
+
+ describe ".resolve!" do
+ let(:current_user) { create(:user) }
+ let!(:commit_note) { create(:diff_note_on_commit) }
+ let!(:resolved_note) { create(:discussion_note_on_merge_request, :resolved) }
+ let!(:unresolved_note) { create(:discussion_note_on_merge_request) }
+
+ before do
+ described_class.resolve!(current_user)
+
+ commit_note.reload
+ resolved_note.reload
+ unresolved_note.reload
+ end
+
+ it 'resolves only the resolvable, not yet resolved notes' do
+ expect(commit_note.resolved_at).to be_nil
+ expect(resolved_note.resolved_by).not_to eq(current_user)
+ expect(unresolved_note.resolved_at).not_to be_nil
+ expect(unresolved_note.resolved_by).to eq(current_user)
+ end
+ end
+
+ describe ".unresolve!" do
+ let!(:resolved_note) { create(:discussion_note_on_merge_request, :resolved) }
+
+ before do
+ described_class.unresolve!
+
+ resolved_note.reload
+ end
+
+ it 'unresolves the resolved notes' do
+ expect(resolved_note.resolved_by).to be_nil
+ expect(resolved_note.resolved_at).to be_nil
+ end
+ end
+
+ describe '#discussion_resolvable?' do
+ # TODO: Test
+ end
+
+ describe '#resolvable?' do
+ context "when potentially resolvable" do
+ before do
+ allow(subject).to receive(:discussion_resolvable?).and_return(true)
+ end
+
+ context "when a system note" do
+ before do
+ subject.system = true
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+
+ context "when a regular note" do
+ it "returns true" do
+ expect(subject.resolvable?).to be true
+ end
+ end
+ end
+
+ context "when not potentially resolvable" do
+ before do
+ allow(subject).to receive(:discussion_resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+ end
+
+ describe "#to_be_resolved?" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when resolved" do
+ before do
+ allow(subject).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when not resolved" do
+ before do
+ allow(subject).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns true" do
+ expect(subject.to_be_resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#resolve!" do
+ let(:current_user) { create(:user) }
+
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.resolve!(current_user)).to be_nil
+ end
+
+ it "doesn't set resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "doesn't set resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "doesn't mark as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when already resolved" do
+ let(:user) { create(:user) }
+
+ before do
+ subject.resolve!(user)
+ end
+
+ it "returns nil" do
+ expect(subject.resolve!(current_user)).to be_nil
+ end
+
+ it "doesn't change resolved_at" do
+ expect(subject.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
+ end
+
+ it "doesn't change resolved_by" do
+ expect(subject.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
+ end
+
+ it "doesn't change resolved status" do
+ expect(subject.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
+ end
+ end
+
+ context "when not yet resolved" do
+ it "returns true" do
+ expect(subject.resolve!(current_user)).to be true
+ end
+
+ it "sets resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to eq(current_user)
+ end
+
+ it "marks as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#unresolve!" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.unresolve!).to be_nil
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when resolved" do
+ let(:user) { create(:user) }
+
+ before do
+ subject.resolve!(user)
+ end
+
+ it "returns true" do
+ expect(subject.unresolve!).to be true
+ end
+
+ it "unsets resolved_at" do
+ subject.unresolve!
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by" do
+ subject.unresolve!
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "unmarks as resolved" do
+ subject.unresolve!
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when not resolved" do
+ it "returns nil" do
+ expect(subject.unresolve!).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 3db5937a4f3..00770d5bcd4 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -31,43 +31,6 @@ describe DiffNote, models: true do
subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
- describe ".resolve!" do
- let(:current_user) { create(:user) }
- let!(:commit_note) { create(:diff_note_on_commit) }
- let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) }
- let!(:unresolved_note) { create(:diff_note_on_merge_request) }
-
- before do
- described_class.resolve!(current_user)
-
- commit_note.reload
- resolved_note.reload
- unresolved_note.reload
- end
-
- it 'resolves only the resolvable, not yet resolved notes' do
- expect(commit_note.resolved_at).to be_nil
- expect(resolved_note.resolved_by).not_to eq(current_user)
- expect(unresolved_note.resolved_at).not_to be_nil
- expect(unresolved_note.resolved_by).to eq(current_user)
- end
- end
-
- describe ".unresolve!" do
- let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) }
-
- before do
- described_class.unresolve!
-
- resolved_note.reload
- end
-
- it 'unresolves the resolved notes' do
- expect(resolved_note.resolved_by).to be_nil
- expect(resolved_note.resolved_at).to be_nil
- end
- end
-
describe "#position=" do
context "when provided a string" do
it "sets the position" do
@@ -226,248 +189,18 @@ describe DiffNote, models: true do
end
end
- describe "#resolvable?" do
+ describe '#discussion_resolvable?' do
context "when noteable is a commit" do
subject { create(:diff_note_on_commit, project: project, position: position) }
it "returns false" do
- expect(subject.resolvable?).to be false
+ expect(subject.discussion_resolvable?).to be false
end
end
context "when noteable is a merge request" do
- context "when a system note" do
- before do
- subject.system = true
- end
-
- it "returns false" do
- expect(subject.resolvable?).to be false
- end
- end
-
- context "when a regular note" do
- it "returns true" do
- expect(subject.resolvable?).to be true
- end
- end
- end
- end
-
- describe "#to_be_resolved?" do
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.to_be_resolved?).to be false
- end
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
- end
-
- context "when resolved" do
- before do
- allow(subject).to receive(:resolved?).and_return(true)
- end
-
- it "returns false" do
- expect(subject.to_be_resolved?).to be false
- end
- end
-
- context "when not resolved" do
- before do
- allow(subject).to receive(:resolved?).and_return(false)
- end
-
- it "returns true" do
- expect(subject.to_be_resolved?).to be true
- end
- end
- end
- end
-
- describe "#resolve!" do
- let(:current_user) { create(:user) }
-
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns nil" do
- expect(subject.resolve!(current_user)).to be_nil
- end
-
- it "doesn't set resolved_at" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_at).to be_nil
- end
-
- it "doesn't set resolved_by" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_by).to be_nil
- end
-
- it "doesn't mark as resolved" do
- subject.resolve!(current_user)
-
- expect(subject.resolved?).to be false
- end
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
- end
-
- context "when already resolved" do
- let(:user) { create(:user) }
-
- before do
- subject.resolve!(user)
- end
-
- it "returns nil" do
- expect(subject.resolve!(current_user)).to be_nil
- end
-
- it "doesn't change resolved_at" do
- expect(subject.resolved_at).not_to be_nil
-
- expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
- end
-
- it "doesn't change resolved_by" do
- expect(subject.resolved_by).to eq(user)
-
- expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
- end
-
- it "doesn't change resolved status" do
- expect(subject.resolved?).to be true
-
- expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
- end
- end
-
- context "when not yet resolved" do
- it "returns true" do
- expect(subject.resolve!(current_user)).to be true
- end
-
- it "sets resolved_at" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_at).not_to be_nil
- end
-
- it "sets resolved_by" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_by).to eq(current_user)
- end
-
- it "marks as resolved" do
- subject.resolve!(current_user)
-
- expect(subject.resolved?).to be true
- end
- end
- end
- end
-
- describe "#unresolve!" do
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns nil" do
- expect(subject.unresolve!).to be_nil
- end
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
- end
-
- context "when resolved" do
- let(:user) { create(:user) }
-
- before do
- subject.resolve!(user)
- end
-
- it "returns true" do
- expect(subject.unresolve!).to be true
- end
-
- it "unsets resolved_at" do
- subject.unresolve!
-
- expect(subject.resolved_at).to be_nil
- end
-
- it "unsets resolved_by" do
- subject.unresolve!
-
- expect(subject.resolved_by).to be_nil
- end
-
- it "unmarks as resolved" do
- subject.unresolve!
-
- expect(subject.resolved?).to be false
- end
- end
-
- context "when not resolved" do
- it "returns nil" do
- expect(subject.unresolve!).to be_nil
- end
- end
- end
- end
-
- describe "#discussion" do
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns nil" do
- expect(subject.discussion).to be_nil
- end
- end
-
- context "when resolvable" do
- let!(:diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: subject.position) }
- let!(:diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: active_position2) }
-
- let(:active_position2) do
- Gitlab::Diff::Position.new(
- old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: 16,
- new_line: 22,
- diff_refs: merge_request.diff_refs
- )
- end
-
- it "returns the discussion this note is in" do
- discussion = subject.discussion
-
- expect(discussion.id).to eq(subject.discussion_id)
- expect(discussion.notes).to eq([subject, diff_note2])
+ it "returns true" do
+ expect(subject.discussion_resolvable?).to be true
end
end
end
diff --git a/spec/models/discussion_note_spec.rb b/spec/models/discussion_note_spec.rb
new file mode 100644
index 00000000000..9b8534e1d61
--- /dev/null
+++ b/spec/models/discussion_note_spec.rb
@@ -0,0 +1,7 @@
+require 'spec_helper'
+
+describe DiscussionNote, models: true do
+ describe '#discussion_id' do
+ # Uses in_reply_to_discussion_id
+ end
+end
diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb
index 2a67c60b978..f42b01810b3 100644
--- a/spec/models/discussion_spec.rb
+++ b/spec/models/discussion_spec.rb
@@ -3,14 +3,14 @@ require 'spec_helper'
describe Discussion, model: true do
subject { described_class.new([first_note, second_note, third_note]) }
- let(:first_note) { create(:diff_note_on_merge_request) }
- let(:second_note) { create(:diff_note_on_merge_request) }
- let(:third_note) { create(:diff_note_on_merge_request) }
+ let(:first_note) { create(:discussion_note_on_merge_request) }
+ let(:second_note) { create(:discussion_note_on_merge_request) }
+ let(:third_note) { create(:discussion_note_on_merge_request) }
describe "#resolvable?" do
- context "when a diff discussion" do
+ context "when potentially resolvable" do
before do
- allow(subject).to receive(:diff_discussion?).and_return(true)
+ allow(subject).to receive(:discussion_resolvable?).and_return(true)
end
context "when all notes are unresolvable" do
@@ -50,9 +50,9 @@ describe Discussion, model: true do
end
end
- context "when not a diff discussion" do
+ context "when not potentially resolvable" do
before do
- allow(subject).to receive(:diff_discussion?).and_return(false)
+ allow(subject).to receive(:discussion_resolvable?).and_return(false)
end
it "returns false" do
@@ -522,9 +522,9 @@ describe Discussion, model: true do
end
describe "#collapsed?" do
- context "when a diff discussion" do
+ context "when potentially resolvable" do
before do
- allow(subject).to receive(:diff_discussion?).and_return(true)
+ allow(subject).to receive(:discussion_resolvable?).and_return(true)
end
context "when resolvable" do
@@ -558,31 +558,43 @@ describe Discussion, model: true do
allow(subject).to receive(:resolvable?).and_return(false)
end
- context "when active" do
+ context "when a diff discussion" do
before do
- allow(subject).to receive(:active?).and_return(true)
+ allow(subject).to receive(:diff_discussion?).and_return(true)
end
- it "returns false" do
- expect(subject.collapsed?).to be false
+ context "when active" do
+ before do
+ allow(subject).to receive(:active?).and_return(true)
+ end
+
+ it "returns false" do
+ expect(subject.collapsed?).to be false
+ end
end
- end
- context "when outdated" do
- before do
- allow(subject).to receive(:active?).and_return(false)
+ context "when outdated" do
+ before do
+ allow(subject).to receive(:active?).and_return(false)
+ end
+
+ it "returns true" do
+ expect(subject.collapsed?).to be true
+ end
end
+ end
- it "returns true" do
- expect(subject.collapsed?).to be true
+ context "when not a diff discussion" do
+ it "returns false" do
+ expect(subject.collapsed?).to be false
end
end
end
end
- context "when not a diff discussion" do
+ context "when not potentially resolvable" do
before do
- allow(subject).to receive(:diff_discussion?).and_return(false)
+ allow(subject).to receive(:discussion_resolvable?).and_return(false)
end
it "returns false" do
@@ -609,4 +621,12 @@ describe Discussion, model: true do
end
end
end
+
+ describe '#single_note?' do
+
+ end
+
+ describe "#reply_attributes" do
+
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 58ccd056328..e3cef6ac37a 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -202,7 +202,7 @@ describe MergeRequest, models: true do
end
end
- describe "#mr_and_commit_notes" do
+ describe "#related_notes" do
let!(:merge_request) { create(:merge_request) }
before do
@@ -214,7 +214,7 @@ describe MergeRequest, models: true do
it "includes notes for commits" do
expect(merge_request.commits).not_to be_empty
- expect(merge_request.mr_and_commit_notes.count).to eq(2)
+ expect(merge_request.related_notes.count).to eq(2)
end
it "includes notes for commits from target project as well" do
@@ -222,7 +222,7 @@ describe MergeRequest, models: true do
project: merge_request.target_project)
expect(merge_request.commits).not_to be_empty
- expect(merge_request.mr_and_commit_notes.count).to eq(3)
+ expect(merge_request.related_notes.count).to eq(3)
end
end
@@ -1119,12 +1119,12 @@ describe MergeRequest, models: true do
end
context "discussion status" do
- let(:first_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
- let(:second_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
- let(:third_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
+ let(:first_discussion) { Discussion.new([create(:discussion_note_on_merge_request)]) }
+ let(:second_discussion) { Discussion.new([create(:discussion_note_on_merge_request)]) }
+ let(:third_discussion) { Discussion.new([create(:discussion_note_on_merge_request)]) }
before do
- allow(subject).to receive(:diff_discussions).and_return([first_discussion, second_discussion, third_discussion])
+ allow(subject).to receive(:resolvable_discussions).and_return([first_discussion, second_discussion, third_discussion])
end
describe "#discussions_resolvable?" do
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 17a15b12dcb..eb6ab34cbbb 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -268,6 +268,14 @@ describe Note, models: true do
end
end
+ describe '.discussions' do
+ # TODO: Test
+ end
+
+ describe '.find_discussion' do
+ # TODO: Test
+ end
+
describe ".grouped_diff_discussions" do
let!(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
@@ -344,4 +352,43 @@ describe Note, models: true do
end
end
end
+
+ describe '#to_discussion' do
+ subject { create(:discussion_note_on_merge_request) }
+ let!(:note2) { create(:discussion_note_on_merge_request, project: subject.project, noteable: subject.noteable, in_reply_to_discussion_id: subject.discussion_id) }
+
+ it "returns a discussion with just this note" do
+ discussion = subject.to_discussion
+
+ expect(discussion.id).to eq(subject.discussion_id)
+ expect(discussion.notes).to eq([subject])
+ end
+ end
+
+ describe "#discussion" do
+ let!(:note1) { create(:discussion_note_on_merge_request) }
+ let!(:note2) { create(:diff_note_on_merge_request, project: note1.project, noteable: note1.noteable) }
+
+ context 'when the note is part of a discussion' do
+ subject { create(:discussion_note_on_merge_request, project: note1.project, noteable: note1.noteable, in_reply_to_discussion_id: note1.discussion_id) }
+
+ it "returns the discussion this note is in" do
+ discussion = subject.discussion
+
+ expect(discussion.id).to eq(subject.discussion_id)
+ expect(discussion.notes).to eq([note1, subject])
+ end
+ end
+
+ context 'when the note is not part of a discussion' do
+ subject { create(:note) }
+
+ it "returns a discussion with just this note" do
+ discussion = subject.discussion
+
+ expect(discussion.id).to eq(subject.discussion_id)
+ expect(discussion.notes).to eq([subject])
+ end
+ end
+ end
end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 25804696d2e..e8b72bc3e14 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -13,6 +13,9 @@ describe Notes::CreateService, services: true do
project.team << [user, :master]
end
+ # TODO: Test in_reply_to_discussion_id
+ # TODO: Test new_discussion
+
context "valid params" do
it 'returns a valid note' do
note = Notes::CreateService.new(project, user, opts).execute