Merge branch '12910-personal-snippets-notes-show' into 'master'

Display comments for personal snippets

See merge request !10974
This commit is contained in:
Sean McGivern 2017-05-02 14:38:28 +00:00
commit d32ecb23eb
28 changed files with 703 additions and 235 deletions

View File

@ -0,0 +1,136 @@
module NotesActions
include RendersNotes
extend ActiveSupport::Concern
included do
before_action :authorize_admin_note!, only: [:update, :destroy]
end
def index
current_fetched_at = Time.now.to_i
notes_json = { notes: [], last_fetched_at: current_fetched_at }
@notes = notes_finder.execute.inc_relations_for_view
@notes = prepare_notes_for_rendering(@notes)
@notes.each do |note|
next if note.cross_reference_not_visible_for?(current_user)
notes_json[:notes] << note_json(note)
end
render json: notes_json
end
def create
create_params = note_params.merge(
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
)
@note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def destroy
if note.editable?
Notes::DestroyService.new(project, current_user).execute(note)
end
respond_to do |format|
format.js { head :ok }
end
end
private
def note_json(note)
attrs = {
commands_changes: note.commands_changes
}
if note.persisted?
attrs.merge!(
valid: true,
id: note.id,
discussion_id: note.discussion_id(noteable),
html: note_html(note),
note: note.note
)
discussion = note.to_discussion(noteable)
unless discussion.individual_note?
attrs.merge!(
discussion_resolvable: discussion.resolvable?,
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
)
end
else
attrs.merge!(
valid: false,
errors: note.errors
)
end
attrs
end
def authorize_admin_note!
return access_denied! unless can?(current_user, :admin_note, note)
end
def note_params
params.require(:note).permit(
:project_id,
:noteable_type,
:noteable_id,
:commit_id,
:noteable,
:type,
:note,
:attachment,
# LegacyDiffNote
:line_code,
# DiffNote
:position
)
end
def noteable
@noteable ||= notes_finder.target
end
def last_fetched_at
request.headers['X-Last-Fetched-At']
end
def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, finder_params)
end
end

View File

@ -10,6 +10,8 @@ module RendersNotes
private
def preload_max_access_for_authors(notes, project)
return nil unless project
user_ids = notes.map(&:author_id)
project.team.max_member_access_for_user_ids(user_ids)
end

View File

@ -22,7 +22,8 @@ module ToggleAwardEmoji
def to_todoable(awardable)
case awardable
when Note
awardable.noteable
# we don't create todos for personal snippet comments for now
awardable.for_personal_snippet? ? nil : awardable.noteable
when MergeRequest, Issue
awardable
when Snippet

View File

@ -1,68 +1,22 @@
class Projects::NotesController < Projects::ApplicationController
include RendersNotes
include NotesActions
include ToggleAwardEmoji
# Authorize
before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
def index
current_fetched_at = Time.now.to_i
notes_json = { notes: [], last_fetched_at: current_fetched_at }
@notes = notes_finder.execute.inc_relations_for_view
@notes = prepare_notes_for_rendering(@notes)
@notes.each do |note|
next if note.cross_reference_not_visible_for?(current_user)
notes_json[:notes] << note_json(note)
end
render json: notes_json
end
#
# This is a fix to make spinach feature tests passing:
# Controller actions are returned from AbstractController::Base and methods of parent classes are
# excluded in order to return only specific controller related methods.
# That is ok for the app (no :create method in ancestors)
# but fails for tests because there is a :create method on FactoryGirl (one of the ancestors)
#
# see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78
#
def create
create_params = note_params.merge(
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
)
@note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def destroy
if note.editable?
Notes::DestroyService.new(project, current_user).execute(note)
end
respond_to do |format|
format.js { head :ok }
end
super
end
def delete_attachment
@ -110,7 +64,7 @@ class Projects::NotesController < Projects::ApplicationController
def note_html(note)
render_to_string(
"projects/notes/_note",
"shared/notes/_note",
layout: false,
formats: [:html],
locals: { note: note }
@ -152,76 +106,11 @@ class Projects::NotesController < Projects::ApplicationController
)
end
def note_json(note)
attrs = {
commands_changes: note.commands_changes
}
if note.persisted?
attrs.merge!(
valid: true,
id: note.id,
discussion_id: note.discussion_id(noteable),
html: note_html(note),
note: note.note
)
discussion = note.to_discussion(noteable)
unless discussion.individual_note?
attrs.merge!(
discussion_resolvable: discussion.resolvable?,
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
)
end
else
attrs.merge!(
valid: false,
errors: note.errors
)
end
attrs
end
def authorize_admin_note!
return access_denied! unless can?(current_user, :admin_note, note)
def finder_params
params.merge(last_fetched_at: last_fetched_at)
end
def authorize_resolve_note!
return access_denied! unless can?(current_user, :resolve_note, note)
end
def note_params
params.require(:note).permit(
:project_id,
:noteable_type,
:noteable_id,
:commit_id,
:noteable,
:type,
:note,
:attachment,
# LegacyDiffNote
:line_code,
# DiffNote
:position
)
end
def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
end
def noteable
@noteable ||= notes_finder.target
end
def last_fetched_at
request.headers['X-Last-Fetched-At']
end
end

View File

@ -0,0 +1,44 @@
class Snippets::NotesController < ApplicationController
include NotesActions
include ToggleAwardEmoji
skip_before_action :authenticate_user!, only: [:index]
before_action :snippet
before_action :authorize_read_snippet!, only: [:show, :index, :create]
private
def note
@note ||= snippet.notes.find(params[:id])
end
alias_method :awardable, :note
def note_html(note)
render_to_string(
"shared/notes/_note",
layout: false,
formats: [:html],
locals: { note: note }
)
end
def project
nil
end
def snippet
PersonalSnippet.find_by(id: params[:snippet_id])
end
def note_params
super.merge(noteable_id: params[:snippet_id])
end
def finder_params
params.merge(last_fetched_at: last_fetched_at, target_id: snippet.id, target_type: 'personal_snippet')
end
def authorize_read_snippet!
return render_404 unless can?(current_user, :read_personal_snippet, snippet)
end
end

View File

@ -1,4 +1,5 @@
class SnippetsController < ApplicationController
include RendersNotes
include ToggleAwardEmoji
include SpammableActions
include SnippetsActions
@ -64,6 +65,11 @@ class SnippetsController < ApplicationController
blob = @snippet.blob
override_max_blob_size(blob)
@noteable = @snippet
@discussions = @snippet.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
respond_to do |format|
format.html do
render 'show'

View File

@ -68,6 +68,8 @@ class NotesFinder
MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
when "snippet", "project_snippet"
SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
when "personal_snippet"
PersonalSnippet.all
else
raise 'invalid target_type'
end

View File

@ -1,10 +1,14 @@
module AwardEmojiHelper
def toggle_award_url(awardable)
return url_for([:toggle_award_emoji, awardable]) unless @project
return url_for([:toggle_award_emoji, awardable]) unless @project || awardable.is_a?(Note)
if awardable.is_a?(Note)
# We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x)
toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id)
if awardable.for_personal_snippet?
toggle_award_emoji_snippet_note_path(awardable.noteable, awardable)
else
toggle_award_emoji_namespace_project_note_path(@project.namespace, @project, awardable.id)
end
else
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
end

View File

@ -281,7 +281,7 @@ class TodoService
def attributes_for_target(target)
attributes = {
project_id: target.project.id,
project_id: target&.project&.id,
target_id: target.id,
target_type: target.class.name,
commit_id: nil

View File

@ -1,6 +1,6 @@
.discussion-notes
%ul.notes{ data: { discussion_id: discussion.id } }
= render partial: "projects/notes/note", collection: discussion.notes, as: :note
= render partial: "shared/notes/note", collection: discussion.notes, as: :note
- if current_user
.discussion-reply-holder

View File

@ -0,0 +1,44 @@
- access = note_max_access_for_user(note)
- if access
%span.note-role= access
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
%resolve-btn{ "project-path" => project_path(note.project),
"discussion-id" => note.discussion_id(@noteable),
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
":author-name" => "'#{j(note.author.name)}'",
"author-avatar" => note.author.avatar_url,
":note-truncated" => "'#{j(truncate(note.note, length: 17))}'",
":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
"ref" => "note_#{note.id}" }
%button.note-action-button.line-resolve-btn{ type: "button",
class: ("is-disabled" unless can_resolve),
":class" => "{ 'is-active': isResolved }",
":aria-label" => "buttonText",
"@click" => "resolve",
":title" => "buttonText",
":ref" => "'button'" }
= icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
%div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg'
- if current_user
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
= link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
= icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil', class: 'link-highlight')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o', class: 'danger-highlight')

View File

@ -0,0 +1,3 @@
.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
#{note.note}
%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note

View File

@ -1,101 +0,0 @@
- return unless note.author
- return if note.cross_reference_not_visible_for?(current_user)
- note_editable = note_editable?(note)
%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} }
.timeline-entry-inner
.timeline-icon
- if note.system
= icon_for_system_note(note)
- else
%a{ href: user_path(note.author) }
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content
.note-header
.note-header-info
%a{ href: user_path(note.author) }
%span.hidden-xs
= sanitize(note.author.name)
%span.note-headline-light
= note.author.to_reference
%span.note-headline-light
%span.note-headline-meta
- unless note.system
commented
- if note.system
%span.system-note-message
= note.redacted_note_html
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system?
.note-actions
- access = note_max_access_for_user(note)
- if access
%span.note-role= access
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
%resolve-btn{ "project-path" => project_path(note.project),
"discussion-id" => note.discussion_id(@noteable),
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
":author-name" => "'#{j(note.author.name)}'",
"author-avatar" => note.author.avatar_url,
":note-truncated" => "'#{j(truncate(note.note, length: 17))}'",
":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
"ref" => "note_#{note.id}" }
%button.note-action-button.line-resolve-btn{ type: "button",
class: ("is-disabled" unless can_resolve),
":class" => "{ 'is-active': isResolved }",
":aria-label" => "buttonText",
"@click" => "resolve",
":title" => "buttonText",
":ref" => "'button'" }
= icon("spin spinner", "v-show" => "loading", class: 'loading')
%div{ 'v-show' => '!loading' }= render "shared/icons/icon_status_success.svg"
- if current_user
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
= link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
= icon('spinner spin')
%span{ class: "link-highlight award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
%span{ class: "link-highlight award-control-icon-positive" }= custom_icon('emoji_smiley')
%span{ class: "link-highlight award-control-icon-super-positive" }= custom_icon('emoji_smile')
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil', class: 'link-highlight')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o', class: 'danger-highlight')
.note-body{ class: note_editable ? 'js-task-list-container' : '' }
.note-text.md
= note.redacted_note_html
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable
.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
#{note.note}
%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system
.system-note-commit-list-toggler
Toggle commit list
%i.fa.fa-angle-down
- if note.attachment.url
.note-attachment
- if note.attachment.image?
= link_to note.attachment.url, target: '_blank' do
= image_tag note.attachment.url, class: 'note-image-attach'
.attachment
= link_to note.attachment.url, target: '_blank' do
= icon('paperclip')
= note.attachment_identifier
= link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note),
title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do
= icon('trash-o', class: 'cred')

View File

@ -1,5 +1,5 @@
%ul#notes-list.notes.main-notes-list.timeline
= render "projects/notes/notes"
= render "shared/notes/notes"
= render 'projects/notes/edit_form'

View File

@ -0,0 +1,62 @@
- return unless note.author
- return if note.cross_reference_not_visible_for?(current_user)
- note_editable = note_editable?(note)
%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} }
.timeline-entry-inner
.timeline-icon
- if note.system
= icon_for_system_note(note)
- else
%a{ href: user_path(note.author) }
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content
.note-header
.note-header-info
%a{ href: user_path(note.author) }
%span.hidden-xs
= sanitize(note.author.name)
%span.note-headline-light
= note.author.to_reference
%span.note-headline-light
%span.note-headline-meta
- unless note.system
commented
- if note.system
%span.system-note-message
= note.redacted_note_html
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system?
.note-actions
- if note.for_personal_snippet?
= render 'snippets/notes/actions', note: note, note_editable: note_editable
- else
= render 'projects/notes/actions', note: note, note_editable: note_editable
.note-body{ class: note_editable ? 'js-task-list-container' : '' }
.note-text.md
= note.redacted_note_html
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable
- if note.for_personal_snippet?
= render 'snippets/notes/edit', note: note
- else
= render 'projects/notes/edit', note: note
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system
.system-note-commit-list-toggler
Toggle commit list
%i.fa.fa-angle-down
- if note.attachment.url
.note-attachment
- if note.attachment.image?
= link_to note.attachment.url, target: '_blank' do
= image_tag note.attachment.url, class: 'note-image-attach'
.attachment
= link_to note.attachment.url, target: '_blank' do
= icon('paperclip')
= note.attachment_identifier
= link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note),
title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do
= icon('trash-o', class: 'cred')

View File

@ -1,8 +1,8 @@
- if defined?(@discussions)
- @discussions.each do |discussion|
- if discussion.individual_note?
= render partial: "projects/notes/note", collection: discussion.notes, as: :note
= render partial: "shared/notes/note", collection: discussion.notes, as: :note
- else
= render 'discussions/discussion', discussion: discussion
- else
= render partial: "projects/notes/note", collection: @notes, as: :note
= render partial: "shared/notes/note", collection: @notes, as: :note

View File

@ -0,0 +1,13 @@
- if current_user
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
= link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
= icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil', class: 'link-highlight')
= link_to snippet_note_path(note.noteable, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o', class: 'danger-highlight')

View File

View File

@ -0,0 +1,2 @@
%ul#notes-list.notes.main-notes-list.timeline
= render "projects/notes/notes"

View File

@ -7,3 +7,6 @@
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
%ul#notes-list.notes.main-notes-list.timeline
#notes= render 'shared/notes/notes'

View File

@ -0,0 +1,4 @@
---
title: Display comments for personal snippets
merge_request:
author:

View File

@ -5,6 +5,14 @@ resources :snippets, concerns: :awardable do
post :mark_as_spam
post :preview_markdown
end
scope module: :snippets do
resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
member do
delete :delete_attachment
end
end
end
end
get '/s/:username', to: redirect('/u/%{username}/snippets'),

View File

@ -167,6 +167,47 @@ describe Projects::NotesController do
end
end
describe 'DELETE destroy' do
let(:request_params) do
{
namespace_id: project.namespace,
project_id: project,
id: note,
format: :js
}
end
context 'user is the author of a note' do
before do
sign_in(note.author)
project.team << [note.author, :developer]
end
it "returns status 200 for html" do
delete :destroy, request_params
expect(response).to have_http_status(200)
end
it "deletes the note" do
expect { delete :destroy, request_params }.to change { Note.count }.from(1).to(0)
end
end
context 'user is not the author of a note' do
before do
sign_in(user)
project.team << [user, :developer]
end
it "returns status 404" do
delete :destroy, request_params
expect(response).to have_http_status(404)
end
end
end
describe 'POST toggle_award_emoji' do
before do
sign_in(user)

View File

@ -0,0 +1,196 @@
require 'spec_helper'
describe Snippets::NotesController do
let(:user) { create(:user) }
let(:private_snippet) { create(:personal_snippet, :private) }
let(:internal_snippet) { create(:personal_snippet, :internal) }
let(:public_snippet) { create(:personal_snippet, :public) }
let(:note_on_private) { create(:note_on_personal_snippet, noteable: private_snippet) }
let(:note_on_internal) { create(:note_on_personal_snippet, noteable: internal_snippet) }
let(:note_on_public) { create(:note_on_personal_snippet, noteable: public_snippet) }
describe 'GET index' do
context 'when a snippet is public' do
before do
note_on_public
get :index, { snippet_id: public_snippet }
end
it "returns status 200" do
expect(response).to have_http_status(200)
end
it "returns not empty array of notes" do
expect(JSON.parse(response.body)["notes"].empty?).to be_falsey
end
end
context 'when a snippet is internal' do
before do
note_on_internal
end
context 'when user not logged in' do
it "returns status 404" do
get :index, { snippet_id: internal_snippet }
expect(response).to have_http_status(404)
end
end
context 'when user logged in' do
before do
sign_in(user)
end
it "returns status 200" do
get :index, { snippet_id: internal_snippet }
expect(response).to have_http_status(200)
end
end
end
context 'when a snippet is private' do
before do
note_on_private
end
context 'when user not logged in' do
it "returns status 404" do
get :index, { snippet_id: private_snippet }
expect(response).to have_http_status(404)
end
end
context 'when user other than author logged in' do
before do
sign_in(user)
end
it "returns status 404" do
get :index, { snippet_id: private_snippet }
expect(response).to have_http_status(404)
end
end
context 'when author logged in' do
before do
note_on_private
sign_in(private_snippet.author)
end
it "returns status 200" do
get :index, { snippet_id: private_snippet }
expect(response).to have_http_status(200)
end
it "returns 1 note" do
get :index, { snippet_id: private_snippet }
expect(JSON.parse(response.body)['notes'].count).to eq(1)
end
end
end
context 'dont show non visible notes' do
before do
note_on_public
sign_in(user)
expect_any_instance_of(Note).to receive(:cross_reference_not_visible_for?).and_return(true)
end
it "does not return any note" do
get :index, { snippet_id: public_snippet }
expect(JSON.parse(response.body)['notes'].count).to eq(0)
end
end
end
describe 'DELETE destroy' do
let(:request_params) do
{
snippet_id: public_snippet,
id: note_on_public,
format: :js
}
end
context 'when user is the author of a note' do
before do
sign_in(note_on_public.author)
end
it "returns status 200" do
delete :destroy, request_params
expect(response).to have_http_status(200)
end
it "deletes the note" do
expect{ delete :destroy, request_params }.to change{ Note.count }.from(1).to(0)
end
context 'system note' do
before do
expect_any_instance_of(Note).to receive(:system?).and_return(true)
end
it "does not delete the note" do
expect{ delete :destroy, request_params }.not_to change{ Note.count }
end
end
end
context 'when user is not the author of a note' do
before do
sign_in(user)
note_on_public
end
it "returns status 404" do
delete :destroy, request_params
expect(response).to have_http_status(404)
end
it "does not update the note" do
expect{ delete :destroy, request_params }.not_to change{ Note.count }
end
end
end
describe 'POST toggle_award_emoji' do
let(:note) { create(:note_on_personal_snippet, noteable: public_snippet) }
before do
sign_in(user)
end
subject { post(:toggle_award_emoji, snippet_id: public_snippet, id: note.id, name: "thumbsup") }
it "toggles the award emoji" do
expect { subject }.to change { note.award_emoji.count }.by(1)
expect(response).to have_http_status(200)
end
it "removes the already awarded emoji when it exists" do
note.toggle_award_emoji('thumbsup', user) # create award emoji before
expect { subject }.to change { AwardEmoji.count }.by(-1)
expect(response).to have_http_status(200)
end
end
end

View File

@ -5,7 +5,7 @@ include ActionDispatch::TestProcess
FactoryGirl.define do
factory :note do
project factory: :empty_project
note "Note"
note { generate(:title) }
author
on_issue

View File

@ -0,0 +1,39 @@
require 'spec_helper'
describe 'Comments on personal snippets', feature: true do
let!(:user) { create(:user) }
let!(:snippet) { create(:personal_snippet, :public) }
let!(:snippet_notes) do
[
create(:note_on_personal_snippet, noteable: snippet, author: user),
create(:note_on_personal_snippet, noteable: snippet)
]
end
let!(:other_note) { create(:note_on_personal_snippet) }
before do
login_as user
visit snippet_path(snippet)
end
subject { page }
context 'viewing the snippet detail page' do
it 'contains notes for a snippet with correct action icons' do
expect(page).to have_selector('#notes-list li', count: 2)
# comment authored by current user
page.within("#notes-list li#note_#{snippet_notes[0].id}") do
expect(page).to have_content(snippet_notes[0].note)
expect(page).to have_selector('.js-note-delete')
expect(page).to have_selector('.note-emoji-button')
end
page.within("#notes-list li#note_#{snippet_notes[1].id}") do
expect(page).to have_content(snippet_notes[1].note)
expect(page).not_to have_selector('.js-note-delete')
expect(page).to have_selector('.note-emoji-button')
end
end
end
end

View File

@ -110,6 +110,15 @@ describe NotesFinder do
expect(notes.count).to eq(1)
end
it 'finds notes on personal snippets' do
note = create(:note_on_personal_snippet)
params = { target_type: 'personal_snippet', target_id: note.noteable_id }
notes = described_class.new(project, user, params).execute
expect(notes.count).to eq(1)
end
it 'raises an exception for an invalid target_type' do
params[:target_type] = 'invalid'
expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type')

View File

@ -0,0 +1,61 @@
require 'spec_helper'
describe AwardEmojiHelper do
describe '.toggle_award_url' do
context 'note on personal snippet' do
let(:note) { create(:note_on_personal_snippet) }
it 'returns correct url' do
expected_url = "/snippets/#{note.noteable.id}/notes/#{note.id}/toggle_award_emoji"
expect(helper.toggle_award_url(note)).to eq(expected_url)
end
end
context 'note on project item' do
let(:note) { create(:note_on_project_snippet) }
it 'returns correct url' do
@project = note.noteable.project
expected_url = "/#{@project.namespace.path}/#{@project.path}/notes/#{note.id}/toggle_award_emoji"
expect(helper.toggle_award_url(note)).to eq(expected_url)
end
end
context 'personal snippet' do
let(:snippet) { create(:personal_snippet) }
it 'returns correct url' do
expected_url = "/snippets/#{snippet.id}/toggle_award_emoji"
expect(helper.toggle_award_url(snippet)).to eq(expected_url)
end
end
context 'merge request' do
let(:merge_request) { create(:merge_request) }
it 'returns correct url' do
@project = merge_request.project
expected_url = "/#{@project.namespace.path}/#{@project.path}/merge_requests/#{merge_request.id}/toggle_award_emoji"
expect(helper.toggle_award_url(merge_request)).to eq(expected_url)
end
end
context 'issue' do
let(:issue) { create(:issue) }
it 'returns correct url' do
@project = issue.project
expected_url = "/#{@project.namespace.path}/#{@project.path}/issues/#{issue.id}/toggle_award_emoji"
expect(helper.toggle_award_url(issue)).to eq(expected_url)
end
end
end
end