mirror of
https://github.com/wahyd4/gitlabhq.git
synced 2026-08-09 04:36:11 +10:00
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
This commit is contained in:
committed by
Robert Speicher
parent
978f0bac8a
commit
48e86ec610
@@ -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)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
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 + "']")
|
||||
@@ -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
|
||||
|
||||
+10
-38
@@ -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
|
||||
|
||||
%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?
|
||||
|
||||
%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
|
||||
|
||||
@@ -1,10 +1,32 @@
|
||||
.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()
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.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
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
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",
|
||||
|
||||
+2
-6
@@ -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
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
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
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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
|
||||
+1
-3
@@ -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
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
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
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
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
|
||||
+12
-75
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user