Commit 48e86ec6 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets Committed by Robert Speicher

Merge branch 'emoji_votes' into 'master'

Award Emoji

This it first iteration of award emoji feature.
We have plan to extend emoji picker by the next release.

For now, you can add award by clicking to the emoji picker or posting a regular comment with emoji like "👍" and any other. You can post not only emoji that listed in the emoji picker.

See merge request !1825
parent 978f0bac
......@@ -56,6 +56,7 @@ v 8.2.0
- Add ability to create milestone in group projects from single form
- Add option to create merge request when editing/creating a file (Dirceu Tiegs)
- Prevent the last owner of a group from being able to delete themselves by 'adding' themselves as a master (James Lopez)
- Add Award Emoji to issue and merge request pages
v 8.1.4
- Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu)
......
class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id) ->
addAward: (emoji) ->
@postEmoji emoji, =>
@addAwardToEmojiBar(emoji)
addAwardToEmojiBar: (emoji, custom_path = '') ->
if @exist(emoji)
if @isActive(emoji)
@decrementCounter(emoji)
else
counter = @findEmojiIcon(emoji).siblings(".counter")
counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass("active")
@addMeToAuthorList(emoji)
else
@createEmoji(emoji, custom_path)
exist: (emoji) ->
@findEmojiIcon(emoji).length > 0
isActive: (emoji) ->
@findEmojiIcon(emoji).parent().hasClass("active")
decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings(".counter")
if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1)
counter.parent().removeClass("active")
@removeMeFromAuthorList(emoji)
else
award = counter.parent()
award.tooltip("destroy")
award.remove()
removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
authors = award_block.attr("data-original-title").split(", ")
authors = _.without(authors, "me").join(", ")
award_block.attr("title", authors)
@resetTooltip(award_block)
addMeToAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
authors = award_block.attr("data-original-title").split(", ")
authors.push("me")
award_block.attr("title", authors.join(", "))
@resetTooltip(award_block)
resetTooltip: (award) ->
award.tooltip("destroy")
# "destroy" call is asynchronous, this is why we need to set timeout.
setTimeout (->
award.tooltip()
), 200
createEmoji: (emoji, custom_path) ->
nodes = []
nodes.push("<div class='award active' title='me'>")
nodes.push("<div class='icon' data-emoji='" + emoji + "'>")
nodes.push(@getImage(emoji, custom_path))
nodes.push("</div>")
nodes.push("<div class='counter'>1")
nodes.push("</div></div>")
$(".awards-controls").before(nodes.join("\n"))
$(".award").tooltip()
getImage: (emoji, custom_path) ->
if custom_path
$(".awards-menu li").first().html().replace(/emoji\/.*\.png/, custom_path)
else
$("li[data-emoji='" + emoji + "']").html()
postEmoji: (emoji, callback) ->
$.post @post_emoji_url, { note: {
note: emoji
noteable_type: @noteable_type
noteable_id: @noteable_id
}},(data) ->
if data.ok
callback.call()
findEmojiIcon: (emoji) ->
$(".icon[data-emoji='" + emoji + "']")
\ No newline at end of file
......@@ -113,13 +113,16 @@ class @Notes
renderNote: (note) ->
# render note if it not present in loaded list
# or skip if rendered
if @isNewNote(note)
if @isNewNote(note) && !note.award
@note_ids.push(note.id)
$('ul.main-notes-list').
append(note.html).
syntaxHighlight()
@initTaskList()
if note.award
awards_handler.addAwardToEmojiBar(note.note, note.emoji_path)
###
Check if note does not exists on page
###
......@@ -255,7 +258,6 @@ class @Notes
###
addNote: (xhr, note, status) =>
@renderNote(note)
@updateVotes()
###
Called in response to the new note form being submitted
......@@ -473,9 +475,6 @@ class @Notes
form = $(e.target).closest(".js-discussion-note-form")
@removeDiscussionNoteForm(form)
updateVotes: ->
true
###
Called after an attachment file has been selected.
......
......@@ -101,3 +101,71 @@
background-color: $background-color;
}
}
.awards {
@include clearfix;
line-height: 34px;
margin: 2px 0;
.award {
@include border-radius(5px);
border: 1px solid;
padding: 0px 10px;
float: left;
margin: 0 5px;
border-color: $border-color;
cursor: pointer;
&.active {
border-color: $border-gray-light;
background-color: $gray-light;
.counter {
font-weight: bold;
}
}
.icon {
float: left;
margin-right: 10px;
}
.counter {
float: left;
}
}
.awards-controls {
margin-left: 10px;
float: left;
.add-award {
font-size: 24px;
color: $gl-gray;
position: relative;
top: 2px;
&:hover,
&:link {
text-decoration: none;
}
}
.awards-menu {
padding: $gl-padding;
min-width: 214px;
> li {
margin: 5px;
}
}
}
.awards-menu{
li {
float: left;
margin: 3px;
}
}
}
......@@ -60,7 +60,7 @@ class Projects::IssuesController < Projects::ApplicationController
def show
@participants = @issue.participants(current_user)
@note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.with_associations.fresh
@notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue
respond_with(@issue)
......
......@@ -254,7 +254,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
# Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request)
@notes = @merge_request.mr_and_commit_notes.inc_author.fresh
@notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh
@discussions = Note.discussions_from_notes(@notes)
@noteable = @merge_request
......
......@@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :find_current_user_notes, except: [:destroy, :delete_attachment]
before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle]
def index
current_fetched_at = Time.now.to_i
......@@ -58,6 +58,27 @@ class Projects::NotesController < Projects::ApplicationController
end
end
def award_toggle
noteable = note_params[:noteable_type] == "issue" ? Issue : MergeRequest
noteable = noteable.find_by!(id: note_params[:noteable_id], project: project)
data = {
author: current_user,
is_award: true,
note: note_params[:note]
}
note = noteable.notes.find_by(data)
if note
note.destroy
else
Notes::CreateService.new(project, current_user, note_params).execute
end
render json: { ok: true }
end
private
def note
......@@ -111,6 +132,9 @@ class Projects::NotesController < Projects::ApplicationController
id: note.id,
discussion_id: note.discussion_id,
html: note_to_html(note),
award: note.is_award,
emoji_path: note.is_award ? ::AwardEmoji.path_to_emoji_image(note.note) : "",
note: note.note,
discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
}
......
......@@ -12,9 +12,9 @@ class NotesFinder
when "commit"
project.notes.for_commit_id(target_id).not_inline
when "issue"
project.issues.find(target_id).notes.inc_author
project.issues.find(target_id).notes.nonawards.inc_author
when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author
when "snippet", "project_snippet"
project.snippets.find(target_id).notes
else
......
......@@ -87,6 +87,31 @@ module IssuesHelper
merge_requests.map(&:to_reference).to_sentence(last_word_connector: ', or ')
end
def url_to_emoji(name)
emoji_path = ::AwardEmoji.path_to_emoji_image(name)
url_to_image(emoji_path)
end
def emoji_author_list(notes, current_user)
list = notes.map do |note|
note.author == current_user ? "me" : note.author.username
end
list.join(", ")
end
def emoji_list
::AwardEmoji::EMOJI_LIST
end
def note_active_class(notes, current_user)
if current_user && notes.pluck(:author_id).include?(current_user.id)
"active"
else
""
end
end
# Required for Gitlab::Markdown::IssueReferenceFilter
module_function :url_for_issue
end
......@@ -89,41 +89,6 @@ module Issuable
opened? || reopened?
end
#
# Votes
#
# Return the number of -1 comments (downvotes)
def downvotes
filter_superceded_votes(notes.select(&:downvote?), notes).size
end
def downvotes_in_percent
if votes_count.zero?
0
else
100.0 - upvotes_in_percent
end
end
# Return the number of +1 comments (upvotes)
def upvotes
filter_superceded_votes(notes.select(&:upvote?), notes).size
end
def upvotes_in_percent
if votes_count.zero?
0
else
100.0 / votes_count * upvotes
end
end
# Return the total number of votes
def votes_count
upvotes + downvotes
end
def subscribed?(user)
subscription = subscriptions.find_by_user_id(user.id)
......@@ -183,18 +148,4 @@ module Issuable
def notes_with_associations
notes.includes(:author, :project)
end
private
def filter_superceded_votes(votes, notes)
filteredvotes = [] + votes
votes.each do |vote|
if vote.superceded?(notes)
filteredvotes.delete(vote)
end
end
filteredvotes
end
end
......@@ -40,16 +40,20 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true
validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' }
validates :commit_id, presence: true, if: ->(n) { n.noteable_type == 'Commit' }
validates :author, presence: true
mount_uploader :attachment, AttachmentUploader
# Scopes
scope :awards, ->{ where(is_award: true) }
scope :nonawards, ->{ where(is_award: false) }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :inline, ->{ where("line_code IS NOT NULL") }
scope :not_inline, ->{ where(line_code: [nil, '']) }
......@@ -97,6 +101,12 @@ class Note < ActiveRecord::Base
def search(query)
where("LOWER(note) like :query", query: "%#{query.downcase}%")
end
def grouped_awards
awards.select(:note).distinct.map do |note|
[ note.note, where(note: note.note) ]
end
end
end
def cross_reference?
......@@ -288,44 +298,6 @@ class Note < ActiveRecord::Base
nil
end
DOWNVOTES = %w(-1 :-1: :thumbsdown: :thumbs_down_sign:)
# Check if the note is a downvote
def downvote?
votable? && note.start_with?(*DOWNVOTES)
end
UPVOTES = %w(+1 :+1: :thumbsup: :thumbs_up_sign:)
# Check if the note is an upvote
def upvote?
votable? && note.start_with?(*UPVOTES)
end
def superceded?(notes)
return false unless vote?
notes.each do |note|
next if note == self
if note.vote? &&
self[:author_id] == note[:author_id] &&
self[:created_at] <= note[:created_at]
return true
end
end
false
end
def vote?
upvote? || downvote?
end
def votable?
for_issue? || (for_merge_request? && !for_diff_line?)
end
# Mentionable override.
def gfm_reference(from_project = nil)
noteable.gfm_reference(from_project)
......
......@@ -5,11 +5,16 @@ module Notes
note.author = current_user
note.system = false
if contains_emoji_only?(params[:note])
note.is_award = true
note.note = emoji_name(params[:note])
end
if note.save
notification_service.new_note(note)
# Skip system notes, like status changes and cross-references.
unless note.system
# Skip system notes, like status changes and cross-references and awards
unless note.system || note.is_award
event_service.leave_note(note, note.author)
note.create_cross_references!
execute_hooks(note)
......@@ -28,5 +33,13 @@ module Notes
note.project.execute_hooks(note_data, :note_hooks)
note.project.execute_services(note_data, :note_hooks)
end
def contains_emoji_only?(note)
note =~ /\A:?[-_+[:alnum:]]*:?\s?\z/
end
def emoji_name(note)
note.match(/\A:?([-_+[:alnum:]]*):?\s?/)[1]
end
end
end
......@@ -102,6 +102,7 @@ class NotificationService
# ignore gitlab service messages
return true if note.note.start_with?('Status changed to closed')
return true if note.cross_reference? && note.system == true
return true if note.is_award
target = note.noteable
......
......@@ -7,7 +7,7 @@
= render 'shared/show_aside'
.gray-content-block.second-block
.gray-content-block.second-block.oneline-block
.row
.col-md-9
.votes-holder.pull-right
......
......@@ -29,8 +29,6 @@
.issue-info
= "#{issue.to_reference} opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe
- if issue.votes_count > 0
= render 'votes/votes_inline', votable: issue
- if issue.milestone
&nbsp;
%span
......
......@@ -34,8 +34,6 @@
.merge-request-info
= "##{merge_request.iid} opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)}".html_safe
- if merge_request.votes_count > 0
= render 'votes/votes_inline', votable: merge_request
- if merge_request.milestone_id?
&nbsp;
%span
......
......@@ -35,26 +35,6 @@
- if note.updated_by && note.updated_by != note.author
by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)}
- if note.superceded?(@notes)
- if note.upvote?
%span.vote.upvote.label.label-gray.strikethrough
= icon('thumbs-up')
\+1
- if note.downvote?
%span.vote.downvote.label.label-gray.strikethrough
= icon('thumbs-down')
\-1
- else
- if note.upvote?
%span.vote.upvote.label.label-success
= icon('thumbs-up')
\+1
- if note.downvote?
%span.vote.downvote.label.label-danger
= icon('thumbs-down')
\-1
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text
= preserve do
......
.votes.votes-block
.btn-group
- unless votable.upvotes.zero?
.btn.btn-sm.disabled.cgreen
%i.fa.fa-thumbs-up
= votable.upvotes
- unless votable.downvotes.zero?
.btn.btn-sm.disabled.cred
%i.fa.fa-thumbs-down
= votable.downvotes
.awards.votes-block
- votable.notes.awards.grouped_awards.each do |emoji, notes|
.award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)}
.icon{"data-emoji" => "#{emoji}"}
= image_tag url_to_emoji(emoji), height: "20px", width: "20px"
.counter
= notes.count
- if current_user
.dropdown.awards-controls
%a.add-award{"data-toggle" => "dropdown", "data-target" => "#", "href" => "#"}
= icon('smile-o')
%ul.dropdown-menu.awards-menu
- emoji_list.each do |emoji|
%li{"data-emoji" => "#{emoji}"}= image_tag url_to_emoji(emoji), height: "20px", width: "20px"
- if current_user
:coffeescript
post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"
noteable_type = "#{votable.class.name.underscore}"
noteable_id = "#{votable.id}"
window.awards_handler = new AwardsHandler(post_emoji_url, noteable_type, noteable_id)
$(".awards-menu li").click (e)->
emoji = $(this).data("emoji")
awards_handler.addAward(emoji)
$(".awards").on "click", ".award", (e)->
emoji = $(this).find(".icon").data("emoji")
awards_handler.addAward(emoji)
$(".award").tooltip()
.votes.votes-inline
- unless votable.upvotes.zero?
%span.upvotes.cgreen
+ #{votable.upvotes}
- unless votable.downvotes.zero?
\/
- unless votable.downvotes.zero?
%span.downvotes.cred
\- #{votable.downvotes}
......@@ -664,6 +664,10 @@ Gitlab::Application.routes.draw do
member do
delete :delete_attachment
end
collection do
post :award_toggle
end
end
resources :uploads, only: [:create] do
......
class AddIsAwardToNotes < ActiveRecord::Migration
def change
add_column :notes, :is_award, :boolean, default: false, null: false
add_index :notes, :is_award
end
end
......@@ -554,12 +554,14 @@ ActiveRecord::Schema.define(version: 20151116144118) do
t.boolean "system", default: false, null: false
t.text "st_diff"
t.integer "updated_by_id"
t.boolean "is_award", default: false, null: false
end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree
add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree
add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
......
......@@ -31,8 +31,6 @@ Parameters:
"project_id": 3,
"title": "test1",
"state": "opened",
"upvotes": 0,
"downvotes": 0,
"author": {
"id": 1,
"username": "admin",
......@@ -77,8 +75,6 @@ Parameters:
"project_id": 3,
"title": "test1",
"state": "merged",
"upvotes": 0,
"downvotes": 0,
"author": {
"id": 1,
"username": "admin",
......@@ -126,8 +122,6 @@ Parameters:
"updated_at": "2015-02-02T20:08:49.959Z",
"target_branch": "secret_token",
"source_branch": "version-1-9",
"upvotes": 0,
"downvotes": 0,
"author": {
"name": "Chad Hamill",
"username": "jarrett",
......@@ -198,8 +192,6 @@ Parameters:
"project_id": 3,
"title": "test1",
"state": "opened",
"upvotes": 0,
"downvotes": 0,
"author": {
"id": 1,
"username": "admin",
......@@ -250,8 +242,6 @@ Parameters:
"title": "test1",
"description": "description1",
"state": "opened",
"upvotes": 0,
"downvotes": 0,
"author": {
"id": 1,
"username": "admin",
......@@ -304,8 +294,6 @@ Parameters:
"project_id": 3,
"title": "test1",
"state": "merged",
"upvotes": 0,
"downvotes": 0,
"author": {
"id": 1,
"username": "admin",
......
......@@ -32,9 +32,7 @@ Parameters:
"created_at": "2013-09-30T13:46:01Z"
},
"created_at": "2013-10-02T09:22:45Z",
"system": true,
"upvote": false,
"downvote": false
"system": true
},
{
"id": 305,
......@@ -49,9 +47,7 @@ Parameters:
"created_at": "2013-09-30T13:46:01Z"
},
"created_at": "2013-10-02T09:56:03Z",
"system": false,
"upvote": false,
"downvote": false
"system": false
}
]
```
......
Feature: Award Emoji
Background:
Given I sign in as a user
And I own project "Shop"
And project "Shop" has issue "Bugfix"
And I visit "Bugfix" issue page
@javascript
Scenario: I add and remove award in the issue
Given I click to emoji-picker
And I click to emoji in the picker
Then I have award added
And I can remove it by clicking to icon
\ No newline at end of file
class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
include SharedAuthentication
include SharedProject
include SharedPaths
include Select2Helper
step 'I visit "Bugfix" issue page' do
visit namespace_project_issue_path(@project.namespace, @project, @issue)
end
step 'I click to emoji-picker' do
page.within ".awards-controls" do
page.find(".add-award").click
end
end
step 'I click to emoji in the picker' do
page.within ".awards-menu" do
page.first("img").click
end
end
step 'I can remove it by clicking to icon' do
page.within ".awards" do
page.first(".award").click
expect(page).to_not have_selector ".award"
end
end
step 'I have award added' do
page.within ".awards" do
expect(page).to have_selector ".award"
expect(page.find(".award .counter")).to have_content "1"
end
end
step 'project "Shop" has issue "Bugfix"' do
@project = Project.find_by(name: "Shop")
@issue = create(:issue, title: "Bugfix", project: project)
end
end
......@@ -162,7 +162,7 @@ module API
end
class MergeRequest < ProjectEntity
expose :target_branch, :source_branch, :upvotes, :downvotes
expose :target_branch, :source_branch
expose :author, :assignee, using: Entities::UserBasic
expose :source_project_id, :target_project_id
expose :label_names, as: :labels
......@@ -192,8 +192,6 @@ module API
expose :author, using: Entities::UserBasic
expose :created_at
expose :system?, as: :system
expose :upvote?, as: :upvote
expose :downvote?, as: :downvote
end
class MRNote < Grape::Entity
......
class AwardEmoji
EMOJI_LIST = [
"+1", "-1", "100", "blush", "heart", "smile", "rage",
"beers", "disappointed", "ok_hand",
"helicopter", "shit", "airplane", "alarm_clock",
"ambulance", "anguished", "two_hearts", "wink"
]
def self.path_to_emoji_image(name)
"emoji/#{Emoji.emoji_filename(name)}.png"
end
end
......@@ -127,4 +127,30 @@ describe IssuesHelper do
it { is_expected.to eq("!1, !2, or !3") }
end
describe "#url_to_emoji" do
it "returns url" do
expect(url_to_emoji("smile")).to include("emoji/1F604.png")
end
end
describe "#emoji_list" do
it "returns url" do
expect(emoji_list).to be_kind_of(Array)
end
end
describe "#note_active_class" do
before do
@note = create :note
@note1 = create :note
end
it "returns empty string for unauthenticated user" do
expect(note_active_class(Note.all, nil)).to eq("")
end
it "returns active string for author" do
expect(note_active_class(Note.all, @note.author)).to eq("active")
end
end
end
require 'spec_helper'
describe Issue, 'Votes' do
let(:issue) { create(:issue) }
describe "#upvotes" do
it "with no notes has a 0/0 score" do
expect(issue.upvotes).to eq(0)
end
it "should recognize non-+1 notes" do
add_note "No +1 here"
expect(issue.notes.size).to eq(1)
expect(issue.notes.first.upvote?).to be_falsey
expect(issue.upvotes).to eq(0)
end
it "should recognize a single +1 note" do
add_note "+1 This is awesome"
expect(issue.upvotes).to eq(1)
end
it 'should recognize multiple +1 notes' do
add_note '+1 This is awesome', create(:user)
add_note '+1 I want this', create(:user)
expect(issue.upvotes).to eq(2)
end
it 'should not count 2 +1 votes from the same user' do
add_note '+1 This is awesome'
add_note '+1 I want this'
expect(issue.upvotes).to eq(1)
end
end
describe "#downvotes" do
it "with no notes has a 0/0 score" do
expect(issue.downvotes).to eq(0)
end
it "should recognize non--1 notes" do
add_note "Almost got a -1"
expect(issue.notes.size).to eq(1)
expect(issue.notes.first.downvote?).to be_falsey
expect(issue.downvotes).to eq(0)
end
it "should recognize a single -1 note" do
add_note "-1 This is bad"
expect(issue.downvotes).to eq(1)
end
it "should recognize multiple -1 notes" do
add_note('-1 This is bad', create(:user))
add_note('-1 Away with this', create(:user))
expect(issue.downvotes).to eq(2)
end
end
describe "#votes_count" do
it "with no notes has a 0/0 score" do
expect(issue.votes_count).to eq(0)
end
it "should recognize non notes" do
add_note "No +1 here"
expect(issue.notes.size).to eq(1)
expect(issue.votes_count).to eq(0)
end
it "should recognize a single +1 note" do
add_note "+1 This is awesome"
expect(issue.votes_count).to eq(1)
end
it "should recognize a single -1 note" do
add_note "-1 This is bad"
expect(issue.votes_count).to eq(1)
end
it "should recognize multiple notes" do
add_note('+1 This is awesome', create(:user))
add_note('-1 This is bad', create(:user))
add_note('+1 I want this', create(:user))
expect(issue.votes_count).to eq(3)
end
it 'should not count 2 -1 votes from the same user' do
add_note '-1 This is suspicious'
add_note '-1 This is bad'
expect(issue.votes_count).to eq(1)
end
end
describe "#upvotes_in_percent" do
it "with no notes has a 0% score" do
expect(issue.upvotes_in_percent).to eq(0)
end
it "should count a single 1 note as 100%" do
add_note "+1 This is awesome"
expect(issue.upvotes_in_percent).to eq(100)
end
it 'should count multiple +1 notes as 100%' do
add_note('+1 This is awesome', create(:user))
add_note('+1 I want this', create(:user))
expect(issue.upvotes_in_percent).to eq(100)
end
it 'should count fractions for multiple +1 and -1 notes correctly' do
add_note('+1 This is awesome', create(:user))
add_note('+1 I want this', create(:user))
add_note('-1 This is bad', create(:user))
add_note('+1 me too', create(:user))
expect(issue.upvotes_in_percent).to eq(75)
end
end
describe "#downvotes_in_percent" do
it "with no notes has a 0% score" do
expect(issue.downvotes_in_percent).to eq(0)
end
it "should count a single -1 note as 100%" do
add_note "-1 This is bad"
expect(issue.downvotes_in_percent).to eq(100)
end
it 'should count multiple -1 notes as 100%' do
add_note('-1 This is bad', create(:user))
add_note('-1 Away with this', create(:user))
expect(issue.downvotes_in_percent).to eq(100)
end
it 'should count fractions for multiple +1 and -1 notes correctly' do
add_note('+1 This is awesome', create(:user))
add_note('+1 I want this', create(:user))
add_note('-1 This is bad', create(:user))
add_note('+1 me too', create(:user))
expect(issue.downvotes_in_percent).to eq(25)
end
end
describe '#filter_superceded_votes' do
it 'should count a users vote only once amongst multiple votes' do
add_note('-1 This needs work before I will accept it')
add_note('+1 I want this', create(:user))
add_note('+1 This is is awesome', create(:user))
add_note('+1 this looks good now')
add_note('+1 This is awesome', create(:user))
add_note('+1 me too', create(:user))
expect(issue.downvotes).to eq(0)
expect(issue.upvotes).to eq(5)
end
it 'should count each users vote only once' do
add_note '-1 This needs work before it will be accepted'
add_note '+1 I like this'
add_note '+1 I still like this'
add_note '+1 I really like this'
add_note '+1 Give me this now!!!!'
expect(issue.downvotes).to eq(0)
expect(issue.upvotes).to eq(1)
end
it 'should count a users vote only once without caring about comments' do
add_note '-1 This needs work before it will be accepted'
add_note 'Comment 1'
add_note 'Another comment'
add_note '+1 vote'
add_note 'final comment'
expect(issue.downvotes).to eq(0)
expect(issue.upvotes).to eq(1)
end
end
def add_note(text, author = issue.author)
created_at = Time.now - 1.hour + Note.count.seconds
issue.notes << create(:note,
note: text,
project: issue.project,
author_id: author.id,
created_at: created_at)
end
end
......@@ -32,77 +32,6 @@ describe Note do
it { is_expected.to validate_presence_of(:project) }
end
describe '#votable?' do
it 'is true for issue notes' do
note = build(:note_on_issue)
expect(note).to be_votable
end
it 'is true for merge request notes' do
note = build(:note_on_merge_request)
expect(note).to be_votable
end
it 'is false for merge request diff notes' do
note = build(:note_on_merge_request_diff)
expect(note).not_to be_votable
end
it 'is false for commit notes' do
note = build(:note_on_commit)
expect(note).not_to be_votable
end
it 'is false for commit diff notes' do
note = build(:note_on_commit_diff)
expect(note).not_to be_votable
end
end
describe 'voting score' do
it 'recognizes a neutral note' do
note = build(:votable_note, note: 'This is not a +1 note')
expect(note).not_to be_upvote
expect(note).not_to be_downvote
end
it 'recognizes a neutral emoji note' do
note = build(:votable_note, note: "I would :+1: this, but I don't want to")
expect(note).not_to be_upvote
expect(note).not_to be_downvote
end
it 'recognizes a +1 note' do
note = build(:votable_note, note: '+1 for this')
expect(note).to be_upvote
end
it 'recognizes a +1 emoji as a vote' do
note = build(:votable_note, note: ':+1: for this')
expect(note).to be_upvote
end
it 'recognizes a thumbsup emoji as a vote' do
note = build(:votable_note, note: ':thumbsup: for this')
expect(note).to be_upvote
end
it 'recognizes a -1 note' do
note = build(:votable_note, note: '-1 for this')
expect(note).to be_downvote
end
it 'recognizes a -1 emoji as a vote' do
note = build(:votable_note, note: ':-1: for this')
expect(note).to be_downvote
end
it 'recognizes a thumbsdown emoji as a vote' do
note = build(:votable_note, note: ':thumbsdown: for this')
expect(note).to be_downvote
end
end
describe "Commit notes" do
let!(:note) { create(:note_on_commit, note: "+1 from me") }
let!(:commit) { note.noteable }
......@@ -139,10 +68,6 @@ describe Note do
it "should be recognized by #for_commit_diff_line?" do
expect(note).to be_for_commit_diff_line
end
it "should not be votable" do
expect(note).not_to be_votable
end
end
describe 'authorization' do
......@@ -204,4 +129,16 @@ describe Note do
it { expect(Note.search('wow')).to include(note) }
end
describe :grouped_awards do
before do
create :note, note: "smile", is_award: true
create :note, note: "smile", is_award: true
end
it "returns grouped array of notes" do
expect(Note.grouped_awards.first.first).to eq("smile")
expect(Note.grouped_awards.first.last).to match_array(Note.all)
end
end
end
......@@ -345,17 +345,6 @@ describe Project do
expect(project1.star_count).to eq(0)
expect(project2.star_count).to eq(0)
end
it 'is decremented when an upvoter account is deleted' do
user = create :user
project = create :project, :public
user.toggle_star(project)
project.reload
expect(project.star_count).to eq(1)
user.destroy
project.reload
expect(project.star_count).to eq(0)
end
end
describe :avatar_type do
......
......@@ -24,4 +24,38 @@ describe Notes::CreateService do
it { expect(@note.note).to eq('Awesome comment') }
end
end
describe "award emoji" do
before do
project.team << [user, :master]
end
it "creates emoji note" do
opts = {
note: ':smile: ',
noteable_type: 'Issue',
noteable_id: issue.id
}
@note = Notes::CreateService.new(project, user, opts).execute
expect(@note).to be_valid
expect(@note.note).to eq('smile')
expect(@note.is_award).to be_truthy
end
it "creates regular note if emoji name is invalid" do
opts = {
note: ':smile: moretext: ',
noteable_type: 'Issue',
noteable_id: issue.id
}
@note = Notes::CreateService.new(project, user, opts).execute
expect(@note).to be_valid
expect(@note.note).to eq(opts[:note])
expect(@note.is_award).to be_falsy
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment