Commit 9440ac66 authored by Jacob Schatz's avatar Jacob Schatz

Merge branch 'awardables' into 'master'

Awardables

TODO:
- [x] Fix specs
- [x] Write new specs
- [X] Check if the migrations can stay offline migrations -- (seems not to make 8.8)
- [X] API expose Awards -> Next merge window

See merge request !3785
parents 2444f3ea 77d4d5e3
class @AwardsHandler class @AwardsHandler
constructor: (@getEmojisUrl, @postEmojiUrl, @noteableType, @noteableId, @unicodes) ->
$('.js-add-award').on 'click', (event) => constructor: ->
@aliases = emojiAliases()
$(document)
.off 'click', '.js-add-award'
.on 'click', '.js-add-award', (event) =>
event.stopPropagation() event.stopPropagation()
event.preventDefault() event.preventDefault()
@showEmojiMenu() @showEmojiMenu $(event.currentTarget)
$('html').on 'click', (event) -> $('html').on 'click', (event) ->
if !$(event.target).closest('.emoji-menu').length unless $(event.target).closest('.emoji-menu').length
if $('.emoji-menu').is(':visible') if $('.emoji-menu').is(':visible')
$('.js-add-award.is-active').removeClass 'is-active'
$('.emoji-menu').removeClass 'is-visible' $('.emoji-menu').removeClass 'is-visible'
$('.awards') $(document)
.off 'click' .off 'click', '.js-emoji-btn'
.on 'click', '.js-emoji-btn', @handleClick .on 'click', '.js-emoji-btn', @handleClick
@renderFrequentlyUsedBlock()
handleClick: (e) -> handleClick: (e) =>
e.preventDefault() e.preventDefault()
emoji = $(this)
.find('.icon')
.data 'emoji'
if emoji is 'thumbsup' and awardsHandler.didUserClickEmoji $(this), 'thumbsdown' emoji = $(e.currentTarget).find('.icon').data 'emoji'
awardsHandler.addAward 'thumbsdown' @getVotesBlock().addClass 'js-awards-block'
@addAward @getAwardUrl(), emoji
else if emoji is 'thumbsdown' and awardsHandler.didUserClickEmoji $(this), 'thumbsup'
awardsHandler.addAward 'thumbsup'
awardsHandler.addAward emoji showEmojiMenu: ($addBtn) ->
$(this).trigger 'blur' $menu = $('.emoji-menu')
didUserClickEmoji: (that, emoji) -> if $menu.length
if $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title') $holder = $addBtn.closest('.js-award-holder')
$(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title').indexOf('me') > -1
showEmojiMenu: -> if $menu.is '.is-visible'
if $('.emoji-menu').length $addBtn.removeClass 'is-active'
if $('.emoji-menu').is '.is-visible' $menu.removeClass 'is-visible'
$('.emoji-menu').removeClass 'is-visible'
$('#emoji_search').blur() $('#emoji_search').blur()
else else
$('.emoji-menu').addClass 'is-visible' $addBtn.addClass 'is-active'
@positionMenu($menu, $addBtn)
$menu.addClass 'is-visible'
$('#emoji_search').focus() $('#emoji_search').focus()
else else
$('.js-add-award').addClass 'is-loading' $addBtn.addClass 'is-loading is-active'
$.get @getEmojisUrl, (response) => url = $addBtn.data 'award-menu-url'
$('.js-add-award').removeClass 'is-loading'
$('.js-award-holder').append response @createEmojiMenu url, =>
$addBtn.removeClass 'is-loading'
$menu = $('.emoji-menu')
@positionMenu($menu, $addBtn)
@renderFrequentlyUsedBlock()
setTimeout => setTimeout =>
$('.emoji-menu').addClass 'is-visible' $menu.addClass 'is-visible'
$('#emoji_search').focus() $('#emoji_search').focus()
@setupSearch() @setupSearch()
, 200 , 200
addAward: (emoji) ->
@postEmoji emoji, => createEmojiMenu: (awardMenuUrl, callback) ->
@addAwardToEmojiBar(emoji)
$.get awardMenuUrl, (response) =>
$('body').append response
callback()
positionMenu: ($menu, $addBtn) ->
position = $addBtn.data('position')
# The menu could potentially be off-screen or in a hidden overflow element
# So we position the element absolute in the body
css =
top: "#{$addBtn.offset().top + $addBtn.outerHeight()}px"
if position? and position is 'right'
css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px"
$menu.addClass 'is-aligned-right'
else
css.left = "#{$addBtn.offset().left}px"
$menu.removeClass 'is-aligned-right'
$menu.css(css)
addAward: (awardUrl, emoji, checkMutuality = yes) ->
emoji = @normilizeEmojiName(emoji)
@postEmoji awardUrl, emoji, =>
@addAwardToEmojiBar(emoji, checkMutuality)
$('.js-awards-block-current').removeClass 'js-awards-block-current'
$('.emoji-menu').removeClass 'is-visible' $('.emoji-menu').removeClass 'is-visible'
addAwardToEmojiBar: (emoji) ->
addAwardToEmojiBar: (emoji, checkForMutuality = yes) ->
@checkMutuality emoji if checkForMutuality
@addEmojiToFrequentlyUsedList(emoji) @addEmojiToFrequentlyUsedList(emoji)
if @exist(emoji) emoji = @normilizeEmojiName(emoji)
if @isActive(emoji) $emojiBtn = @findEmojiIcon(emoji).parent()
@decrementCounter(emoji)
if $emojiBtn.length > 0
if @isActive($emojiBtn)
@decrementCounter($emojiBtn, emoji)
else else
counter = @findEmojiIcon(emoji).siblings('.js-counter') counter = $emojiBtn.find('.js-counter')
counter.text(parseInt(counter.text()) + 1) counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass('active') $emojiBtn.addClass('active')
@addMeToAuthorList(emoji) @addMeToUserList(emoji)
else else
@createEmoji(emoji) @createEmoji(emoji)
exist: (emoji) ->
@findEmojiIcon(emoji).length > 0 getVotesBlock: -> return $ '.awards.js-awards-block'
isActive: (emoji) ->
@findEmojiIcon(emoji).parent().hasClass('active') getAwardUrl: -> @getVotesBlock().data 'award-url'
decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings('.js-counter') checkMutuality: (emoji) ->
emojiIcon = counter.parent()
if parseInt(counter.text()) > 1 awardUrl = @getAwardUrl()
counter.text(parseInt(counter.text()) - 1)
emojiIcon.removeClass('active') if emoji in [ 'thumbsup', 'thumbsdown' ]
@removeMeFromAuthorList(emoji) mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup'
else if emoji == 'thumbsup' || emoji == 'thumbsdown'
emojiIcon.tooltip('destroy') isAlreadyVoted = $("[data-emoji=#{mutualVote}]").parent().hasClass 'active'
counter.text(0) @addAward awardUrl, mutualVote, no if isAlreadyVoted
emojiIcon.removeClass('active')
@removeMeFromAuthorList(emoji)
isActive: ($emojiBtn) -> $emojiBtn.hasClass 'active'
decrementCounter: ($emojiBtn, emoji) ->
isntNoteBody = $emojiBtn.closest('.note-body').length is 0
counter = $('.js-counter', $emojiBtn)
counterNumber = parseInt(counter.text())
if !isntNoteBody
# If this is a note body, we just hide the award emoji row like the initial state
$emojiBtn.closest('.js-awards-block').addClass 'hidden'
if counterNumber > 1
counter.text(counterNumber - 1)
@removeMeFromUserList($emojiBtn, emoji)
else if (emoji == 'thumbsup' || emoji == 'thumbsdown') && isntNoteBody
$emojiBtn.tooltip('destroy')
counter.text('0')
@removeMeFromUserList($emojiBtn, emoji)
else else
emojiIcon.tooltip('destroy') $emojiBtn.tooltip('destroy')
emojiIcon.remove() $emojiBtn.remove()
$emojiBtn.removeClass('active')
getAwardTooltip: ($awardBlock) ->
return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title')
removeMeFromUserList: ($emojiBtn, emoji) ->
awardBlock = $emojiBtn
originalTitle = @getAwardTooltip awardBlock
authors = originalTitle.split ', '
authors.splice authors.indexOf('me'), 1
newAuthors = authors.join ', '
removeMeFromAuthorList: (emoji) ->
awardBlock = @findEmojiIcon(emoji).parent()
authors = awardBlock
.attr('data-original-title')
.split(', ')
authors.splice(authors.indexOf('me'),1)
awardBlock awardBlock
.closest('.js-emoji-btn') .closest '.js-emoji-btn'
.attr('data-original-title', authors.join(', ')) .removeData 'original-title'
.removeData 'title'
.attr 'data-original-title', newAuthors
.attr 'data-title', newAuthors
@resetTooltip(awardBlock) @resetTooltip(awardBlock)
addMeToAuthorList: (emoji) ->
addMeToUserList: (emoji) ->
awardBlock = @findEmojiIcon(emoji).parent() awardBlock = @findEmojiIcon(emoji).parent()
origTitle = awardBlock.attr('data-original-title').trim() origTitle = @getAwardTooltip awardBlock
authors = [] users = []
if origTitle if origTitle
authors = origTitle.split(', ') users = origTitle.trim().split(', ')
authors.push('me')
awardBlock.attr('data-original-title', authors.join(', ')) users.push('me')
awardBlock.attr('title', users.join(', '))
@resetTooltip(awardBlock) @resetTooltip(awardBlock)
resetTooltip: (award) -> resetTooltip: (award) ->
award.tooltip('destroy') award.tooltip('destroy')
# "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. # 'destroy' call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout.
setTimeout (-> setTimeout (->
award.tooltip() award.tooltip()
), 200 ), 200
createEmoji: (emoji) -> createEmoji_: (emoji) ->
emojiCssClass = @resolveNameToCssClass(emoji)
emojiCssClass = @resolveNameToCssClass emoji
nodes = []
nodes.push( buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'>
"<button class='btn award-control js-emoji-btn has-tooltip active' data-original-title='me'>", <div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>
"<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>", <span class='award-control-text js-counter'>1</span>
"<span class='award-control-text js-counter'>1</span>", </button>"
"</button>"
) emoji_node = $(buttonHtml)
.insertBefore '.js-awards-block .js-award-holder:not(.js-award-action-btn)'
$(nodes.join("\n")) .find '.emoji-icon'
.insertBefore('.js-award-holder') .data 'emoji', emoji
.find('.emoji-icon')
.data('emoji', emoji)
$('.award-control').tooltip() $('.award-control').tooltip()
$currentBlock = $ '.js-awards-block'
if $currentBlock.is '.hidden'
$currentBlock.removeClass 'hidden'
createEmoji: (emoji) ->
return @createEmoji_ emoji if $('.emoji-menu').length
awardMenuUrl = gl.awardMenuUrl or '/emojis'
@createEmojiMenu awardMenuUrl, => @createEmoji emoji
resolveNameToCssClass: (emoji) -> resolveNameToCssClass: (emoji) ->
emojiIcon = $(".emoji-menu-content [data-emoji='#{emoji}']")
if emojiIcon.length > 0 emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
unicodeName = emojiIcon.data('unicode-name')
if emoji_icon.length > 0
unicodeName = emoji_icon.data('unicode-name')
else else
# Find by alias # Find by alias
unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name') unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name')
"emoji-#{unicodeName}" return "emoji-#{unicodeName}"
postEmoji: (emoji, callback) ->
$.post @postEmojiUrl, { note: { postEmoji: (awardUrl, emoji, callback) ->
note: ":#{emoji}:" $.post awardUrl, { name: emoji }, (data) ->
noteable_type: @noteableType
noteable_id: @noteableId
}},(data) ->
if data.ok if data.ok
callback.call() callback.call()
findEmojiIcon: (emoji) -> findEmojiIcon: (emoji) ->
$(".awards > .js-emoji-btn [data-emoji='#{emoji}']") $(".js-awards-block.awards > .js-emoji-btn [data-emoji='#{emoji}']")
scrollToAwards: -> scrollToAwards: ->
$('body, html').animate({ $('body, html').animate({
scrollTop: $('.awards').offset().top - 80 scrollTop: $('.awards').offset().top - 80
}, 200) }, 200)
normilizeEmojiName: (emoji) ->
@aliases[emoji] || emoji
addEmojiToFrequentlyUsedList: (emoji) -> addEmojiToFrequentlyUsedList: (emoji) ->
frequentlyUsedEmojis = @getFrequentlyUsedEmojis() frequently_used_emojis = @getFrequentlyUsedEmojis()
frequentlyUsedEmojis.push(emoji) frequently_used_emojis.push(emoji)
$.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }) $.cookie('frequently_used_emojis', frequently_used_emojis.join(','), { expires: 365 })
getFrequentlyUsedEmojis: -> getFrequentlyUsedEmojis: ->
frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',') frequently_used_emojis = ($.cookie('frequently_used_emojis') || '').split(',')
_.compact(_.uniq(frequentlyUsedEmojis)) _.compact(_.uniq(frequently_used_emojis))
renderFrequentlyUsedBlock: -> renderFrequentlyUsedBlock: ->
if $.cookie('frequently_used_emojis') if $.cookie('frequently_used_emojis')
frequentlyUsedEmojis = @getFrequentlyUsedEmojis() frequently_used_emojis = @getFrequentlyUsedEmojis()
ul = $('<ul>') ul = $("<ul class='clearfix emoji-menu-list'>")
for emoji in frequentlyUsedEmojis for emoji in frequently_used_emojis
do (emoji) ->
$(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul) $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
$('input.emoji-search').after(ul).after($('<h5>').text('Frequently used')) $('input.emoji-search').after(ul).after($('<h5>').text('Frequently used'))
setupSearch: -> setupSearch: ->
$('input.emoji-search').keyup (ev) => $('input.emoji-search').on 'keyup', (ev) =>
term = $(ev.target).val() term = $(ev.target).val()
# Clean previous search results # Clean previous search results
...@@ -204,12 +303,12 @@ class @AwardsHandler ...@@ -204,12 +303,12 @@ class @AwardsHandler
if term if term
# Generate a search result block # Generate a search result block
h5 = $('<h5>').text('Search results').addClass('emoji-search') h5 = $('<h5>').text('Search results').addClass('emoji-search')
foundEmojis = @searchEmojis(term).show() found_emojis = @searchEmojis(term).show()
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis) ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis)
$('.emoji-menu-content ul, .emoji-menu-content h5').hide() $('.emoji-menu-content ul, .emoji-menu-content h5').hide()
$('.emoji-menu-content').append(h5).append(ul) $('.emoji-menu-content').append(h5).append(ul)
else else
$('.emoji-menu-content').children().show() $('.emoji-menu-content').children().show()
searchEmojis: (term)-> searchEmojis: (term)->
$(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone() $(".emoji-menu-content [data-emoji*='#{term}']").closest('li').clone()
...@@ -22,6 +22,7 @@ class Dispatcher ...@@ -22,6 +22,7 @@ class Dispatcher
new Issue() new Issue()
shortcut_handler = new ShortcutsIssuable() shortcut_handler = new ShortcutsIssuable()
new ZenMode() new ZenMode()
window.awardsHandler = new AwardsHandler()
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone() new Milestone()
when 'dashboard:todos:index' when 'dashboard:todos:index'
...@@ -52,6 +53,7 @@ class Dispatcher ...@@ -52,6 +53,7 @@ class Dispatcher
new Diff() new Diff()
shortcut_handler = new ShortcutsIssuable(true) shortcut_handler = new ShortcutsIssuable(true)
new ZenMode() new ZenMode()
window.awardsHandler = new AwardsHandler()
when "projects:merge_requests:diffs" when "projects:merge_requests:diffs"
new Diff() new Diff()
new ZenMode() new ZenMode()
......
window.emojiAliases = ->
JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
...@@ -167,7 +167,7 @@ class @Notes ...@@ -167,7 +167,7 @@ class @Notes
return return
if note.award if note.award
awardsHandler.addAwardToEmojiBar(note.note) awardsHandler.addAwardToEmojiBar(note.name)
awardsHandler.scrollToAwards() awardsHandler.scrollToAwards()
# render note if it not present in loaded list # render note if it not present in loaded list
......
.awards { .awards {
line-height: 34px;
.emoji-icon { .emoji-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
...@@ -9,8 +7,6 @@ ...@@ -9,8 +7,6 @@
.emoji-menu { .emoji-menu {
position: absolute; position: absolute;
top: 100%;
left: 0;
margin-top: 3px; margin-top: 3px;
z-index: 1000; z-index: 1000;
min-width: 160px; min-width: 160px;
...@@ -23,7 +19,12 @@ ...@@ -23,7 +19,12 @@
opacity: 0; opacity: 0;
transform: scale(.2); transform: scale(.2);
transform-origin: 0 -45px; transform-origin: 0 -45px;
transition: all .3s cubic-bezier(.87,-.41,.19,1.44); transition: .3s cubic-bezier(.87,-.41,.19,1.44);
transition-property: transform, opacity;
&.is-aligned-right {
transform-origin: 100% -45px;
}
&.is-visible { &.is-visible {
pointer-events: all; pointer-events: all;
...@@ -107,7 +108,7 @@ ...@@ -107,7 +108,7 @@
} }
&.is-loading { &.is-loading {
.award-control-icon { .award-control-icon-normal {
display: none; display: none;
} }
......
module ToggleAwardEmoji
extend ActiveSupport::Concern
included do
before_action :authenticate_user!, only: [:toggle_award_emoji]
end
def toggle_award_emoji
name = params.require(:name)
awardable.toggle_award_emoji(name, current_user)
TodoService.new.new_award_emoji(awardable, current_user)
render json: { ok: true }
end
private
def awardable
raise NotImplementedError
end
end
class Projects::IssuesController < Projects::ApplicationController class Projects::IssuesController < Projects::ApplicationController
include ToggleSubscriptionAction include ToggleSubscriptionAction
include IssuableActions include IssuableActions
include ToggleAwardEmoji
before_action :module_enabled before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
...@@ -62,7 +63,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -62,7 +63,7 @@ class Projects::IssuesController < Projects::ApplicationController
def show def show
@note = @project.notes.new(noteable: @issue) @note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.nonawards.with_associations.fresh @notes = @issue.notes.with_associations.fresh
@noteable = @issue @noteable = @issue
respond_to do |format| respond_to do |format|
...@@ -169,6 +170,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -169,6 +170,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
alias_method :subscribable_resource, :issue alias_method :subscribable_resource, :issue
alias_method :issuable, :issue alias_method :issuable, :issue
alias_method :awardable, :issue
def authorize_read_issue! def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue) return render_404 unless can?(current_user, :read_issue, @issue)
......
...@@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
include ToggleSubscriptionAction include ToggleSubscriptionAction
include DiffHelper include DiffHelper
include IssuableActions include IssuableActions
include ToggleAwardEmoji
before_action :module_enabled before_action :module_enabled
before_action :merge_request, only: [ before_action :merge_request, only: [
...@@ -270,6 +271,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -270,6 +271,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
alias_method :subscribable_resource, :merge_request alias_method :subscribable_resource, :merge_request
alias_method :issuable, :merge_request alias_method :issuable, :merge_request
alias_method :awardable, :merge_request
def closes_issues def closes_issues
@closes_issues ||= @merge_request.closes_issues @closes_issues ||= @merge_request.closes_issues
...@@ -305,7 +307,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -305,7 +307,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_show_vars def define_show_vars
# Build a note object for comment form # Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request) @note = @project.notes.new(noteable: @merge_request)
@notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh @notes = @merge_request.mr_and_commit_notes.inc_author.fresh
@discussions = @notes.discussions @discussions = @notes.discussions
@noteable = @merge_request @noteable = @merge_request
......
...@@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_read_note! before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create] before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy] before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle] before_action :find_current_user_notes, only: [:index]
def index def index
current_fetched_at = Time.now.to_i current_fetched_at = Time.now.to_i
...@@ -56,30 +56,6 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -56,30 +56,6 @@ class Projects::NotesController < Projects::ApplicationController
end end
end end
def award_toggle
noteable = if note_params[:noteable_type] == "issue"
project.issues.find(note_params[:noteable_id])
else
project.merge_requests.find(note_params[:noteable_id])
end
data = {
author: current_user,
is_award: true,
note: note_params[:note].delete(":")
}
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 private
def note def note
...@@ -131,13 +107,20 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -131,13 +107,20 @@ class Projects::NotesController < Projects::ApplicationController
end end
def note_json(note) def note_json(note)
if note.valid? if note.is_a?(AwardEmoji)
{
valid: note.valid?,
award: true,
id: note.id,
name: note.name
}
elsif note.valid?
{ {
valid: true, valid: true,
id: note.id, id: note.id,
discussion_id: note.discussion_id, discussion_id: note.discussion_id,
html: note_to_html(note), html: note_to_html(note),
award: note.is_award, award: false,
note: note.note, note: note.note,
discussion_html: note_to_discussion_html(note), discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note) discussion_with_diff_html: note_to_discussion_with_diff_html(note)
...@@ -145,7 +128,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -145,7 +128,7 @@ class Projects::NotesController < Projects::ApplicationController
else else
{ {
valid: false, valid: false,
award: note.is_award, award: false,
errors: note.errors errors: note.errors
} }
end end
......
...@@ -139,7 +139,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -139,7 +139,7 @@ class ProjectsController < Projects::ApplicationController
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id) participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = { @suggestions = {
emojis: AwardEmoji.urls, emojis: Gitlab::AwardEmoji.urls,
issues: autocomplete.issues, issues: autocomplete.issues,
milestones: autocomplete.milestones, milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests, mergerequests: autocomplete.merge_requests,
......
...@@ -12,9 +12,9 @@ class NotesFinder ...@@ -12,9 +12,9 @@ class NotesFinder
when "commit" when "commit"
project.notes.for_commit_id(target_id).non_diff_notes project.notes.for_commit_id(target_id).non_diff_notes
when "issue" when "issue"
project.issues.find(target_id).notes.nonawards.inc_author project.issues.find(target_id).notes.inc_author
when "merge_request" when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
when "snippet", "project_snippet" when "snippet", "project_snippet"
project.snippets.find(target_id).notes project.snippets.find(target_id).notes
else else
......
...@@ -145,16 +145,14 @@ module IssuesHelper ...@@ -145,16 +145,14 @@ module IssuesHelper
end end
end end
def emoji_author_list(notes, current_user) def award_user_list(awards, current_user)
list = notes.map do |note| awards.map do |award|
note.author == current_user ? "me" : note.author.name award.user == current_user ? 'me' : award.user.name
end.join(', ')
end end
list.join(", ") def award_active_class(awards, current_user)
end if current_user && awards.find { |a| a.user_id == current_user.id }
def note_active_class(notes, current_user)
if current_user && notes.pluck(:author_id).include?(current_user.id)
"active" "active"
else else
"" ""
......
class AwardEmoji < ActiveRecord::Base
DOWNVOTE_NAME = "thumbsdown".freeze
UPVOTE_NAME = "thumbsup".freeze
include Participable
belongs_to :awardable, polymorphic: true
belongs_to :user
validates :awardable, :user, presence: true
validates :name, presence: true, inclusion: { in: Emoji.emojis_names }
validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] }
participant :user
scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
scope :upvotes, -> { where(name: UPVOTE_NAME) }
def downvote?
self.name == DOWNVOTE_NAME
end
def upvote?
self.name == UPVOTE_NAME
end
end
module Awardable
extend ActiveSupport::Concern
included do
has_many :award_emoji, as: :awardable, dependent: :destroy
if self < Participable
participant :award_emoji
end
end
module ClassMethods
def order_upvotes_desc
order_votes_desc(AwardEmoji::UPVOTE_NAME)
end
def order_downvotes_desc
order_votes_desc(AwardEmoji::DOWNVOTE_NAME)
end
def order_votes_desc(emoji_name)
awardable_table = self.arel_table
awards_table = AwardEmoji.arel_table
join_clause = awardable_table.join(awards_table, Arel::Nodes::OuterJoin).on(
awards_table[:awardable_id].eq(awardable_table[:id]).and(
awards_table[:awardable_type].eq(self.name).and(
awards_table[:name].eq(emoji_name)
)
)
).join_sources
joins(join_clause).group(awardable_table[:id]).reorder("COUNT(award_emoji.id) DESC")
end
end
def grouped_awards(with_thumbs: true)
awards = award_emoji.group_by(&:name)
if with_thumbs
awards[AwardEmoji::UPVOTE_NAME] ||= []
awards[AwardEmoji::DOWNVOTE_NAME] ||= []
end
awards
end
def downvotes
award_emoji.downvotes.count
end
def upvotes
award_emoji.upvotes.count
end
def emoji_awardable?
true
end
def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: current_user).exists?
end
def create_award_emoji(name, current_user)
return unless emoji_awardable?
award_emoji.create(name: name, user: current_user)
end
def remove_award_emoji(name, current_user)
award_emoji.where(name: name, user: current_user).destroy_all
end
def toggle_award_emoji(emoji_name, current_user)
if awarded_emoji?(emoji_name, current_user)
remove_award_emoji(emoji_name, current_user)
else
create_award_emoji(emoji_name, current_user)
end
end
end
...@@ -10,6 +10,7 @@ module Issuable ...@@ -10,6 +10,7 @@ module Issuable
include Mentionable include Mentionable
include Subscribable include Subscribable
include StripAttribute include StripAttribute
include Awardable
included do included do
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
...@@ -115,29 +116,6 @@ module Issuable ...@@ -115,29 +116,6 @@ module Issuable
end end
end end
def order_downvotes_desc
order_votes_desc('thumbsdown')
end
def order_upvotes_desc
order_votes_desc('thumbsup')
end
def order_votes_desc(award_emoji_name)
issuable_table = self.arel_table
note_table = Note.arel_table
join_clause = issuable_table.join(note_table, Arel::Nodes::OuterJoin).on(
note_table[:noteable_id].eq(issuable_table[:id]).and(
note_table[:noteable_type].eq(self.name).and(
note_table[:is_award].eq(true).and(note_table[:note].eq(award_emoji_name))
)
)
).join_sources
joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC")
end
def with_label(title, sort = nil) def with_label(title, sort = nil)
if title.is_a?(Array) && title.size > 1 if title.is_a?(Array) && title.size > 1
joins(:labels).where(labels: { title: title }).group(*grouping_columns(sort)).having("COUNT(DISTINCT labels.title) = #{title.size}") joins(:labels).where(labels: { title: title }).group(*grouping_columns(sort)).having("COUNT(DISTINCT labels.title) = #{title.size}")
...@@ -179,14 +157,6 @@ module Issuable ...@@ -179,14 +157,6 @@ module Issuable
opened? || reopened? opened? || reopened?
end end
def downvotes
notes.awards.where(note: "thumbsdown").count
end
def upvotes
notes.awards.where(note: "thumbsup").count
end
def user_notes_count def user_notes_count
notes.user.count notes.user.count
end end
......
...@@ -110,6 +110,10 @@ class LegacyDiffNote < Note ...@@ -110,6 +110,10 @@ class LegacyDiffNote < Note
@active @active
end end
def award_emoji_supported?
false
end
private private
def find_diff def find_diff
......
...@@ -21,11 +21,8 @@ class Note < ActiveRecord::Base ...@@ -21,11 +21,8 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true delegate :name, :email, to: :author, prefix: true
delegate :title, to: :noteable, allow_nil: true delegate :title, to: :noteable, allow_nil: true
before_validation :set_award!
validates :note, :project, presence: true validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award }
# Attachments are deprecated and are handled by Markdown uploader # Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size } validates :attachment, file_size: { maximum: :max_attachment_size }
...@@ -43,8 +40,6 @@ class Note < ActiveRecord::Base ...@@ -43,8 +40,6 @@ class Note < ActiveRecord::Base
mount_uploader :attachment, AttachmentUploader mount_uploader :attachment, AttachmentUploader
# Scopes # 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 :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :system, ->{ where(system: true) } scope :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) } scope :user, ->{ where(system: false) }
...@@ -109,19 +104,6 @@ class Note < ActiveRecord::Base ...@@ -109,19 +104,6 @@ class Note < ActiveRecord::Base
found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE') found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE')
end end
end end
def grouped_awards
notes = {}
awards.select(:note).distinct.map do |note|
notes[note.note] = where(note: note.note)
end
notes["thumbsup"] ||= Note.none
notes["thumbsdown"] ||= Note.none
notes
end
end end
def cross_reference? def cross_reference?
...@@ -205,44 +187,24 @@ class Note < ActiveRecord::Base ...@@ -205,44 +187,24 @@ class Note < ActiveRecord::Base
Event.reset_event_cache_for(self) Event.reset_event_cache_for(self)
end end
def downvote?
is_award && note == "thumbsdown"
end
def upvote?
is_award && note == "thumbsup"
end
def editable? def editable?
!system? && !is_award !system?
end end
def cross_reference_not_visible_for?(user) def cross_reference_not_visible_for?(user)
cross_reference? && referenced_mentionables(user).empty? cross_reference? && referenced_mentionables(user).empty?
end end
# Checks if note is an award added as a comment def award_emoji?
# award_emoji_supported? && contains_emoji_only?
# If note is an award, this method sets is_award to true
# and changes content of the note to award name.
#
# Method is executed as a before_validation callback.
#
def set_award!
return unless awards_supported? && contains_emoji_only?
self.is_award = true
self.note = award_emoji_name
end end
private
def clear_blank_line_code! def clear_blank_line_code!
self.line_code = nil if self.line_code.blank? self.line_code = nil if self.line_code.blank?
end end
def awards_supported? def award_emoji_supported?
(for_issue? || for_merge_request?) && !diff_note? noteable.is_a?(Awardable)
end end
def contains_emoji_only? def contains_emoji_only?
...@@ -251,6 +213,6 @@ class Note < ActiveRecord::Base ...@@ -251,6 +213,6 @@ class Note < ActiveRecord::Base
def award_emoji_name def award_emoji_name
original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
AwardEmoji.normilize_emoji_name(original_name) Gitlab::AwardEmoji.normalize_emoji_name(original_name)
end end
end end
...@@ -84,6 +84,7 @@ class User < ActiveRecord::Base ...@@ -84,6 +84,7 @@ class User < ActiveRecord::Base
has_many :builds, dependent: :nullify, class_name: 'Ci::Build' has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
has_many :todos, dependent: :destroy has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy has_many :notification_settings, dependent: :destroy
has_many :award_emoji, as: :awardable, dependent: :destroy
# #
# Validations # Validations
......
...@@ -24,6 +24,7 @@ module Issues ...@@ -24,6 +24,7 @@ module Issues
@new_issue = create_new_issue @new_issue = create_new_issue
rewrite_notes rewrite_notes
rewrite_award_emoji
add_note_moved_from add_note_moved_from
# Old issue tasks # Old issue tasks
...@@ -72,6 +73,14 @@ module Issues ...@@ -72,6 +73,14 @@ module Issues
end end
end end
def rewrite_award_emoji
@old_issue.award_emoji.each do |award|
new_award = award.dup
new_award.awardable = @new_issue
new_award.save
end
end
def rewrite_content(content) def rewrite_content(content)
return unless content return unless content
......
...@@ -5,6 +5,13 @@ module Notes ...@@ -5,6 +5,13 @@ module Notes
note.author = current_user note.author = current_user
note.system = false note.system = false
if note.award_emoji?
noteable = note.noteable
todo_service.new_award_emoji(noteable, current_user)
return noteable.create_award_emoji(note.award_emoji_name, current_user)
end
if note.save if note.save
# Finish the harder work in the background # Finish the harder work in the background
NewNoteWorker.perform_in(2.seconds, note.id, params) NewNoteWorker.perform_in(2.seconds, note.id, params)
......
...@@ -8,7 +8,7 @@ module Notes ...@@ -8,7 +8,7 @@ module Notes
def execute def execute
# Skip system notes, like status changes and cross-references and awards # Skip system notes, like status changes and cross-references and awards
unless @note.system || @note.is_award unless @note.system?
EventCreateService.new.leave_note(@note, @note.author) EventCreateService.new.leave_note(@note, @note.author)
@note.create_cross_references! @note.create_cross_references!
execute_note_hooks execute_note_hooks
......
...@@ -130,8 +130,7 @@ class NotificationService ...@@ -130,8 +130,7 @@ class NotificationService
# ignore gitlab service messages # ignore gitlab service messages
return true if note.note.start_with?('Status changed to closed') return true if note.note.start_with?('Status changed to closed')
return true if note.cross_reference? && note.system == true return true if note.cross_reference? && note.system?
return true if note.is_award
target = note.noteable target = note.noteable
......
...@@ -122,6 +122,14 @@ class TodoService ...@@ -122,6 +122,14 @@ class TodoService
handle_note(note, current_user) handle_note(note, current_user)
end end
# When an emoji is awarded we should:
#
# * mark all pending todos related to the awardable for the current user as done
#
def new_award_emoji(awardable, current_user)
mark_pending_todos_as_done(awardable, current_user)
end
# When marking pending todos as done we should: # When marking pending todos as done we should:
# #
# * mark all pending todos related to the target for the current user as done # * mark all pending todos related to the target for the current user as done
......
- grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji)
%span.award-control-text.js-counter
= awards.count
- if current_user
:javascript
gl.awardMenuUrl = "#{emojis_path}"
.award-menu-holder.js-award-holder
%button.btn.award-control.js-add-award{ type: "button", data: { award_menu_url: emojis_path } }
= icon('smile-o', class: "award-control-icon award-control-icon-normal")
= icon('spinner spin', class: "award-control-icon award-control-icon-loading")
%span.award-control-text
Add
.emoji-menu .emoji-menu
.emoji-menu-content .emoji-menu-content
= text_field_tag :emoji_search, "", class: "emoji-search search-input form-control" = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
- AwardEmoji.emoji_by_category.each do |category, emojis| - Gitlab::AwardEmoji.emoji_by_category.each do |category, emojis|
%h5.emoji-menu-title %h5.emoji-menu-title
= AwardEmoji::CATEGORIES[category] = Gitlab::AwardEmoji::CATEGORIES[category]
%ul.clearfix.emoji-menu-list %ul.clearfix.emoji-menu-list
- emojis.each do |emoji| - emojis.each do |emoji|
%li.pull-left.text-center.emoji-menu-list-item %li.pull-left.text-center.emoji-menu-list-item
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
= icon('thumbs-down') = icon('thumbs-down')
= downvotes = downvotes
- note_count = issue.notes.user.nonawards.count - note_count = issue.notes.user.count
%li %li
= link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do = link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do
= icon('comments') = icon('comments')
......
...@@ -70,7 +70,7 @@ ...@@ -70,7 +70,7 @@
.content-block.content-block-small .content-block.content-block-small
= render 'new_branch' = render 'new_branch'
= render 'votes/votes_block', votable: @issue = render 'award_emoji/awards_block', awardable: @issue, inline: true
%section.issuable-discussion %section.issuable-discussion
= render 'projects/issues/discussion' = render 'projects/issues/discussion'
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
= icon('thumbs-down') = icon('thumbs-down')
= downvotes = downvotes
- note_count = merge_request.mr_and_commit_notes.user.nonawards.count - note_count = merge_request.mr_and_commit_notes.user.count
%li %li
= link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do
= icon('comments') = icon('comments')
......
...@@ -6,4 +6,3 @@ ...@@ -6,4 +6,3 @@
- if @merge_requests.present? - if @merge_requests.present?
= paginate @merge_requests, theme: "gitlab" = paginate @merge_requests, theme: "gitlab"
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
%li.notes-tab %li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
Discussion Discussion
%span.badge= @merge_request.mr_and_commit_notes.user.nonawards.count %span.badge= @merge_request.mr_and_commit_notes.user.count
%li.commits-tab %li.commits-tab
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits Commits
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
.tab-content .tab-content
#notes.notes.tab-pane.voting_notes #notes.notes.tab-pane.voting_notes
.content-block.content-block-small.oneline-block .content-block.content-block-small.oneline-block
= render 'votes/votes_block', votable: @merge_request = render 'award_emoji/awards_block', awardable: @merge_request, inline: true
.row .row
%section.col-md-12 %section.col-md-12
......
.awards.votes-block
- awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes|
%button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), data: {placement: "top", original_title: emoji_author_list(notes, current_user)}}
= emoji_icon(emoji, sprite: false)
%span.award-control-text.js-counter
= notes.count
- if current_user
%div.award-menu-holder.js-award-holder
%a.btn.award-control.js-add-award{"href" => "#"}
= icon('smile-o', {class: "award-control-icon"})
= icon('spinner spin', {class: "award-control-icon award-control-icon-loading"})
%span.award-control-text
Add
- if current_user
:javascript
var getEmojisUrl = "#{emojis_path}";
var postEmojiUrl = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}";
var noteableType = "#{votable.class.name.underscore}";
var noteableId = "#{votable.id}";
var unicodes = #{AwardEmoji.unicode.to_json};
window.awardsHandler = new AwardsHandler(
getEmojisUrl,
postEmojiUrl,
noteableType,
noteableId,
unicodes
);
...@@ -8,3 +8,7 @@ ...@@ -8,3 +8,7 @@
# inflect.irregular 'person', 'people' # inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep ) # inflect.uncountable %w( fish sheep )
# end # end
#
ActiveSupport::Inflector.inflections do |inflect|
inflect.uncountable %w(award_emoji)
end
...@@ -652,6 +652,7 @@ Rails.application.routes.draw do ...@@ -652,6 +652,7 @@ Rails.application.routes.draw do
post :cancel_merge_when_build_succeeds post :cancel_merge_when_build_succeeds
get :ci_status get :ci_status
post :toggle_subscription post :toggle_subscription
post :toggle_award_emoji
post :remove_wip post :remove_wip
end end
...@@ -727,6 +728,7 @@ Rails.application.routes.draw do ...@@ -727,6 +728,7 @@ Rails.application.routes.draw do
resources :issues, constraints: { id: /\d+/ } do resources :issues, constraints: { id: /\d+/ } do
member do member do
post :toggle_subscription post :toggle_subscription
post :toggle_award_emoji
get :referenced_merge_requests get :referenced_merge_requests
get :related_branches get :related_branches
get :can_create_branch get :can_create_branch
...@@ -757,10 +759,6 @@ Rails.application.routes.draw do ...@@ -757,10 +759,6 @@ Rails.application.routes.draw do
member do member do
delete :delete_attachment delete :delete_attachment
end end
collection do
post :award_toggle
end
end end
resources :uploads, only: [:create] do resources :uploads, only: [:create] do
......
class AddAwardEmoji < ActiveRecord::Migration
def change
create_table :award_emoji do |t|
t.string :name
t.references :user
t.references :awardable, polymorphic: true
t.timestamps
end
add_index :award_emoji, :user_id
add_index :award_emoji, [:awardable_type, :awardable_id]
end
end
class ConvertAwardNoteToEmojiAward < ActiveRecord::Migration
def change
def up
execute "INSERT INTO award_emoji (awardable_type, awardable_id, user_id, name, created_at, updated_at) (SELECT noteable_type, noteable_id, author_id, note, created_at, updated_at FROM notes WHERE is_award = true)"
execute "DELETE FROM notes WHERE is_award = true"
end
end
end
class RemoveNoteIsAward < ActiveRecord::Migration
def change
remove_column :notes, :is_award, :boolean
end
end
...@@ -100,6 +100,18 @@ ActiveRecord::Schema.define(version: 20160530150109) do ...@@ -100,6 +100,18 @@ ActiveRecord::Schema.define(version: 20160530150109) do
add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree
create_table "award_emoji", force: :cascade do |t|
t.string "name"
t.integer "user_id"
t.integer "awardable_id"
t.string "awardable_type"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "award_emoji", ["awardable_type", "awardable_id"], name: "index_award_emoji_on_awardable_type_and_awardable_id", using: :btree
add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree
create_table "broadcast_messages", force: :cascade do |t| create_table "broadcast_messages", force: :cascade do |t|
t.text "message", null: false t.text "message", null: false
t.datetime "starts_at" t.datetime "starts_at"
...@@ -638,7 +650,6 @@ ActiveRecord::Schema.define(version: 20160530150109) do ...@@ -638,7 +650,6 @@ ActiveRecord::Schema.define(version: 20160530150109) do
t.boolean "system", default: false, null: false t.boolean "system", default: false, null: false
t.text "st_diff" t.text "st_diff"
t.integer "updated_by_id" t.integer "updated_by_id"
t.boolean "is_award", default: false, null: false
t.string "type" t.string "type"
end end
...@@ -646,7 +657,6 @@ ActiveRecord::Schema.define(version: 20160530150109) do ...@@ -646,7 +657,6 @@ ActiveRecord::Schema.define(version: 20160530150109) do
add_index "notes", ["commit_id"], name: "index_notes_on_commit_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", "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", ["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", ["line_code"], name: "index_notes_on_line_code", using: :btree
add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"} add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"}
add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
......
...@@ -191,15 +191,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps ...@@ -191,15 +191,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end end
step 'issue "Release 0.4" have 2 upvotes and 1 downvote' do step 'issue "Release 0.4" have 2 upvotes and 1 downvote' do
issue = Issue.find_by(title: 'Release 0.4') awardable = Issue.find_by(title: 'Release 0.4')
create_list(:upvote_note, 2, project: project, noteable: issue) create_list(:award_emoji, 2, awardable: awardable)
create(:downvote_note, project: project, noteable: issue) create(:award_emoji, :downvote, awardable: awardable)
end end
step 'issue "Tweet control" have 1 upvote and 2 downvotes' do step 'issue "Tweet control" have 1 upvote and 2 downvotes' do
issue = Issue.find_by(title: 'Tweet control') awardable = Issue.find_by(title: 'Tweet control')
create(:upvote_note, project: project, noteable: issue) create(:award_emoji, :upvote, awardable: awardable)
create_list(:downvote_note, 2, project: project, noteable: issue) create_list(:award_emoji, 2, awardable: awardable, name: 'thumbsdown')
end end
step 'The list should be sorted by "Least popular"' do step 'The list should be sorted by "Least popular"' do
......
...@@ -179,14 +179,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -179,14 +179,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do
merge_request = MergeRequest.find_by(title: 'Bug NS-04') merge_request = MergeRequest.find_by(title: 'Bug NS-04')
create_list(:upvote_note, 2, project: project, noteable: merge_request) create_list(:award_emoji, 2, awardable: merge_request)
create(:downvote_note, project: project, noteable: merge_request) create(:award_emoji, :downvote, awardable: merge_request)
end end
step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do
merge_request = MergeRequest.find_by(title: 'Bug NS-06') awardable = MergeRequest.find_by(title: 'Bug NS-06')
create(:upvote_note, project: project, noteable: merge_request) create(:award_emoji, awardable: awardable)
create_list(:downvote_note, 2, project: project, noteable: merge_request) create_list(:award_emoji, 2, :downvote, awardable: awardable)
end end
step 'The list should be sorted by "Least popular"' do step 'The list should be sorted by "Least popular"' do
......
...@@ -171,10 +171,12 @@ module API ...@@ -171,10 +171,12 @@ module API
expose :label_names, as: :labels expose :label_names, as: :labels
expose :milestone, using: Entities::Milestone expose :milestone, using: Entities::Milestone
expose :assignee, :author, using: Entities::UserBasic expose :assignee, :author, using: Entities::UserBasic
expose :subscribed do |issue, options| expose :subscribed do |issue, options|
issue.subscribed?(options[:current_user]) issue.subscribed?(options[:current_user])
end end
expose :user_notes_count expose :user_notes_count
expose :upvotes, :downvotes
end end
class MergeRequest < ProjectEntity class MergeRequest < ProjectEntity
...@@ -217,8 +219,8 @@ module API ...@@ -217,8 +219,8 @@ module API
expose :system?, as: :system expose :system?, as: :system
expose :noteable_id, :noteable_type expose :noteable_id, :noteable_type
# upvote? and downvote? are deprecated, always return false # upvote? and downvote? are deprecated, always return false
expose :upvote?, as: :upvote expose(:upvote?) { |note| false }
expose :downvote?, as: :downvote expose(:downvote?) { |note| false }
end end
class MRNote < Grape::Entity class MRNote < Grape::Entity
......
class AwardEmoji
CATEGORIES = {
other: "Other",
objects: "Objects",
places: "Places",
travel_places: "Travel",
emoticons: "Emoticons",
objects_symbols: "Symbols",
nature: "Nature",
celebration: "Celebration",
people: "People",
activity: "Activity",
flags: "Flags",
food_drink: "Food"
}.with_indifferent_access
CATEGORY_ALIASES = {
symbols: "objects_symbols",
foods: "food_drink",
travel: "travel_places"
}.with_indifferent_access
def self.normilize_emoji_name(name)
aliases[name] || name
end
def self.emoji_by_category
unless @emoji_by_category
@emoji_by_category = Hash.new { |h, key| h[key] = [] }
emojis.each do |emoji_name, data|
data["name"] = emoji_name
# Skip Fitzpatrick(tone) modifiers
next if data["category"] == "modifier"
category = CATEGORY_ALIASES[data["category"]] || data["category"]
@emoji_by_category[category] << data
end
@emoji_by_category = @emoji_by_category.sort.to_h
end
@emoji_by_category
end
def self.emojis
@emojis ||= begin
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' )
JSON.parse(File.read(json_path))
end
end
def self.unicode
@unicode ||= emojis.map {|key, value| { key => emojis[key]["unicode"] } }.inject(:merge!)
end
def self.aliases
@aliases ||= begin
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' )
JSON.parse(File.read(json_path))
end
end
# Returns an Array of Emoji names and their asset URLs.
def self.urls
@urls ||= begin
path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
prefix = Gitlab::Application.config.assets.prefix
digest = Gitlab::Application.config.assets.digest
JSON.parse(File.read(path)).map do |hash|
if digest
fname = "#{hash['unicode']}-#{hash['digest']}"
else
fname = hash['unicode']
end
{ name: hash['name'], path: "#{prefix}/#{fname}.png" }
end
end
end
end
module Gitlab
class AwardEmoji
CATEGORIES = {
other: "Other",
objects: "Objects",
places: "Places",
travel_places: "Travel",
emoticons: "Emoticons",
objects_symbols: "Symbols",
nature: "Nature",
celebration: "Celebration",
people: "People",
activity: "Activity",
flags: "Flags",
food_drink: "Food"
}.with_indifferent_access
CATEGORY_ALIASES = {
symbols: "objects_symbols",
foods: "food_drink",
travel: "travel_places"
}.with_indifferent_access
def self.normalize_emoji_name(name)
aliases[name] || name
end
def self.emoji_by_category
unless @emoji_by_category
@emoji_by_category = Hash.new { |h, key| h[key] = [] }
emojis.each do |emoji_name, data|
data["name"] = emoji_name
# Skip Fitzpatrick(tone) modifiers
next if data["category"] == "modifier"
category = CATEGORY_ALIASES[data["category"]] || data["category"]
@emoji_by_category[category] << data
end
@emoji_by_category = @emoji_by_category.sort.to_h
end
@emoji_by_category
end
def self.emojis
@emojis ||=
begin
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' )
JSON.parse(File.read(json_path))
end
end
def self.aliases
@aliases ||=
begin
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' )
JSON.parse(File.read(json_path))
end
end
# Returns an Array of Emoji names and their asset URLs.
def self.urls
@urls ||= begin
path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
prefix = Gitlab::Application.config.assets.prefix
digest = Gitlab::Application.config.assets.digest
JSON.parse(File.read(path)).map do |hash|
if digest
fname = "#{hash['unicode']}-#{hash['digest']}"
else
fname = hash['unicode']
end
{ name: hash['name'], path: "#{prefix}/#{fname}.png" }
end
end
end
end
end
...@@ -31,9 +31,9 @@ describe GroupsController do ...@@ -31,9 +31,9 @@ describe GroupsController do
let(:issue_2) { create(:issue, project: project) } let(:issue_2) { create(:issue, project: project) }
before do before do
create_list(:upvote_note, 3, project: project, noteable: issue_2) create_list(:award_emoji, 3, awardable: issue_2)
create_list(:upvote_note, 2, project: project, noteable: issue_1) create_list(:award_emoji, 2, awardable: issue_1)
create_list(:downvote_note, 2, project: project, noteable: issue_2) create_list(:award_emoji, 2, :downvote, awardable: issue_2,)
sign_in(user) sign_in(user)
end end
...@@ -56,9 +56,9 @@ describe GroupsController do ...@@ -56,9 +56,9 @@ describe GroupsController do
let(:merge_request_2) { create(:merge_request, :simple, source_project: project) } let(:merge_request_2) { create(:merge_request, :simple, source_project: project) }
before do before do
create_list(:upvote_note, 3, project: project, noteable: merge_request_2) create_list(:award_emoji, 3, awardable: merge_request_2)
create_list(:upvote_note, 2, project: project, noteable: merge_request_1) create_list(:award_emoji, 2, awardable: merge_request_1)
create_list(:downvote_note, 2, project: project, noteable: merge_request_2) create_list(:award_emoji, 2, :downvote, awardable: merge_request_2)
sign_in(user) sign_in(user)
end end
......
...@@ -250,4 +250,20 @@ describe Projects::IssuesController do ...@@ -250,4 +250,20 @@ describe Projects::IssuesController do
end end
end end
end end
describe 'POST #toggle_award_emoji' do
before do
sign_in(user)
project.team << [user, :developer]
end
it "toggles the award emoji" do
expect do
post(:toggle_award_emoji, namespace_id: project.namespace.path,
project_id: project.path, id: issue.iid, name: "thumbsup")
end.to change { issue.award_emoji.count }.by(1)
expect(response.status).to eq(200)
end
end
end end
FactoryGirl.define do
factory :award_emoji do
name "thumbsup"
user
awardable factory: :issue
trait :upvote
trait :downvote do
name "thumbsdown"
end
end
end
...@@ -16,8 +16,6 @@ FactoryGirl.define do ...@@ -16,8 +16,6 @@ FactoryGirl.define do
factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff], class: LegacyDiffNote factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff], class: LegacyDiffNote
factory :note_on_project_snippet, traits: [:on_project_snippet] factory :note_on_project_snippet, traits: [:on_project_snippet]
factory :system_note, traits: [:system] factory :system_note, traits: [:system]
factory :downvote_note, traits: [:award, :downvote]
factory :upvote_note, traits: [:award, :upvote]
trait :on_commit do trait :on_commit do
noteable nil noteable nil
...@@ -46,10 +44,6 @@ FactoryGirl.define do ...@@ -46,10 +44,6 @@ FactoryGirl.define do
system true system true
end end
trait :award do
is_award true
end
trait :downvote do trait :downvote do
note "thumbsdown" note "thumbsdown"
end end
......
...@@ -28,7 +28,6 @@ describe 'Awards Emoji', feature: true do ...@@ -28,7 +28,6 @@ describe 'Awards Emoji', feature: true do
end end
context 'click the thumbsup emoji' do context 'click the thumbsup emoji' do
it 'should increment the thumbsup emoji', js: true do it 'should increment the thumbsup emoji', js: true do
find('[data-emoji="thumbsup"]').click find('[data-emoji="thumbsup"]').click
sleep 2 sleep 2
...@@ -41,7 +40,6 @@ describe 'Awards Emoji', feature: true do ...@@ -41,7 +40,6 @@ describe 'Awards Emoji', feature: true do
end end
context 'click the thumbsdown emoji' do context 'click the thumbsdown emoji' do
it 'should increment the thumbsdown emoji', js: true do it 'should increment the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click find('[data-emoji="thumbsdown"]').click
sleep 2 sleep 2
......
require 'rails_helper'
feature 'Issue awards', js: true, feature: true do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
describe 'logged in' do
before do
login_as(user)
visit namespace_project_issue_path(project.namespace, project, issue)
end
it 'should add award to issue' do
first('.js-emoji-btn').click
expect(page).to have_selector('.js-emoji-btn.active')
expect(first('.js-emoji-btn')).to have_content '1'
visit namespace_project_issue_path(project.namespace, project, issue)
expect(first('.js-emoji-btn')).to have_content '1'
end
it 'should remove award from issue' do
first('.js-emoji-btn').click
find('.js-emoji-btn.active').click
expect(first('.js-emoji-btn')).to have_content '0'
visit namespace_project_issue_path(project.namespace, project, issue)
expect(first('.js-emoji-btn')).to have_content '0'
end
it 'should only have one menu on the page' do
first('.js-add-award').click
expect(page).to have_selector('.emoji-menu')
expect(page).to have_selector('.emoji-menu', count: 1)
end
end
describe 'logged out' do
before do
visit namespace_project_issue_path(project.namespace, project, issue)
end
it 'should not see award menu button' do
expect(page).not_to have_selector('.js-award-holder')
end
end
end
...@@ -125,7 +125,7 @@ describe 'Issues', feature: true do ...@@ -125,7 +125,7 @@ describe 'Issues', feature: true do
describe 'Issue info' do describe 'Issue info' do
it 'excludes award_emoji from comment count' do it 'excludes award_emoji from comment count' do
issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar') issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar')
create(:upvote_note, noteable: issue, project: project) create(:award_emoji, awardable: issue)
visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id) visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
......
require 'rails_helper'
feature 'Merge request awards', js: true, feature: true do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, source_project: project) }
describe 'logged in' do
before do
login_as(user)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
it 'should add award to merge request' do
first('.js-emoji-btn').click
expect(page).to have_selector('.js-emoji-btn.active')
expect(first('.js-emoji-btn')).to have_content '1'
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(first('.js-emoji-btn')).to have_content '1'
end
it 'should remove award from merge request' do
first('.js-emoji-btn').click
find('.js-emoji-btn.active').click
expect(first('.js-emoji-btn')).to have_content '0'
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(first('.js-emoji-btn')).to have_content '0'
end
it 'should only have one menu on the page' do
first('.js-add-award').click
expect(page).to have_selector('.emoji-menu')
expect(page).to have_selector('.emoji-menu', count: 1)
end
end
describe 'logged out' do
before do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
it 'should not see award menu button' do
expect(page).not_to have_selector('.js-award-holder')
end
end
end
...@@ -4,20 +4,6 @@ describe 'Comments', feature: true do ...@@ -4,20 +4,6 @@ describe 'Comments', feature: true do
include RepoHelpers include RepoHelpers
include WaitForAjax include WaitForAjax
describe 'On merge requests page', feature: true do
it 'excludes award_emoji from comment count' do
merge_request = create(:merge_request)
project = merge_request.source_project
create(:upvote_note, noteable: merge_request, project: project)
login_as :admin
visit namespace_project_merge_requests_path(project.namespace, project)
expect(merge_request.mr_and_commit_notes.count).to eq 1
expect(page.all('.merge-request-no-comments').first.text).to eq "0"
end
end
describe 'On a merge request', js: true, feature: true do describe 'On a merge request', js: true, feature: true do
let!(:project) { create(:project) } let!(:project) { create(:project) }
let!(:merge_request) do let!(:merge_request) do
...@@ -147,17 +133,6 @@ describe 'Comments', feature: true do ...@@ -147,17 +133,6 @@ describe 'Comments', feature: true do
end end
end end
end end
describe 'comment info' do
it 'excludes award_emoji from comment count' do
create(:upvote_note, noteable: merge_request, project: project)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(merge_request.mr_and_commit_notes.count).to eq 2
expect(find('.notes-tab span.badge').text).to eq "1"
end
end
end end
describe 'On a merge request diff', js: true, feature: true do describe 'On a merge request diff', js: true, feature: true do
......
...@@ -163,18 +163,15 @@ describe IssuesHelper do ...@@ -163,18 +163,15 @@ describe IssuesHelper do
it { is_expected.to eq("!1, !2, or !3") } it { is_expected.to eq("!1, !2, or !3") }
end end
describe "note_active_class" do describe '#award_active_class' do
before do let!(:upvote) { create(:award_emoji) }
@note = create :note
@note1 = create :note
end
it "returns empty string for unauthenticated user" do it "returns empty string for unauthenticated user" do
expect(note_active_class(Note.all, nil)).to eq("") expect(award_active_class(AwardEmoji.all, nil)).to eq("")
end end
it "returns active string for author" do it "returns active string for author" do
expect(note_active_class(Note.all, @note.author)).to eq("active") expect(award_active_class(AwardEmoji.all, upvote.user)).to eq("active")
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe AwardEmoji do describe Gitlab::AwardEmoji do
describe '.urls' do describe '.urls' do
subject { AwardEmoji.urls } subject { Gitlab::AwardEmoji.urls }
it { is_expected.to be_an_instance_of(Array) } it { is_expected.to be_an_instance_of(Array) }
it { is_expected.not_to be_empty } it { is_expected.not_to be_empty }
...@@ -19,7 +19,7 @@ describe AwardEmoji do ...@@ -19,7 +19,7 @@ describe AwardEmoji do
describe '.emoji_by_category' do describe '.emoji_by_category' do
it "only contains known categories" do it "only contains known categories" do
undefined_categories = AwardEmoji.emoji_by_category.keys - AwardEmoji::CATEGORIES.keys undefined_categories = Gitlab::AwardEmoji.emoji_by_category.keys - Gitlab::AwardEmoji::CATEGORIES.keys
expect(undefined_categories).to be_empty expect(undefined_categories).to be_empty
end end
end end
......
require 'spec_helper'
describe AwardEmoji, models: true do
describe 'Associations' do
it { is_expected.to belong_to(:awardable) }
it { is_expected.to belong_to(:user) }
end
describe 'modules' do
it { is_expected.to include_module(Participable) }
end
describe "validations" do
it { is_expected.to validate_presence_of(:awardable) }
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:name) }
# To circumvent a bug in the shoulda matchers
describe "scoped uniqueness validation" do
it "rejects duplicate award emoji" do
user = create(:user)
issue = create(:issue)
create(:award_emoji, user: user, awardable: issue)
new_award = build(:award_emoji, user: user, awardable: issue)
expect(new_award).not_to be_valid
end
end
end
end
require 'spec_helper'
describe Issue, "Awardable" do
let!(:issue) { create(:issue) }
let!(:award_emoji) { create(:award_emoji, :downvote, awardable: issue) }
describe "Associations" do
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
end
describe "ClassMethods" do
let!(:issue2) { create(:issue) }
before do
create(:award_emoji, awardable: issue2)
end
it "orders on upvotes" do
expect(Issue.order_upvotes_desc.to_a).to eq [issue2, issue]
end
it "orders on downvotes" do
expect(Issue.order_downvotes_desc.to_a).to eq [issue, issue2]
end
end
describe "#upvotes" do
it "counts the number of upvotes" do
expect(issue.upvotes).to be 0
end
end
describe "#downvotes" do
it "counts the number of downvotes" do
expect(issue.downvotes).to be 1
end
end
describe "#toggle_award_emoji" do
it "adds an emoji if it isn't awarded yet" do
expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by 1
end
it "toggles already awarded emoji" do
expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by -1
end
end
end
...@@ -12,6 +12,10 @@ describe Issue, "Issuable" do ...@@ -12,6 +12,10 @@ describe Issue, "Issuable" do
it { is_expected.to have_many(:todos).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) }
end end
describe 'Included modules' do
it { is_expected.to include_module(Awardable) }
end
describe "Validation" do describe "Validation" do
before do before do
allow(subject).to receive(:set_iid).and_return(false) allow(subject).to receive(:set_iid).and_return(false)
...@@ -245,8 +249,8 @@ describe Issue, "Issuable" do ...@@ -245,8 +249,8 @@ describe Issue, "Issuable" do
let(:project) { issue.project } let(:project) { issue.project }
before do before do
issue.notes.awards.create!(note: "thumbsup", author: user, project: project) create(:award_emoji, :upvote, awardable: issue)
issue.notes.awards.create!(note: "thumbsdown", author: user, project: project) create(:award_emoji, :downvote, awardable: issue)
end end
it "returns correct values" do it "returns correct values" do
......
...@@ -171,23 +171,6 @@ describe Note, models: true do ...@@ -171,23 +171,6 @@ describe Note, models: true do
end end
end 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 hash of notes" do
expect(Note.grouped_awards.keys.size).to eq(3)
expect(Note.grouped_awards["smile"]).to match_array(Note.all)
end
it "returns thumbsup and thumbsdown always" do
expect(Note.grouped_awards["thumbsup"]).to match_array(Note.none)
expect(Note.grouped_awards["thumbsdown"]).to match_array(Note.none)
end
end
describe "editable?" do describe "editable?" do
it "returns true" do it "returns true" do
note = build(:note) note = build(:note)
...@@ -198,11 +181,6 @@ describe Note, models: true do ...@@ -198,11 +181,6 @@ describe Note, models: true do
note = build(:note, system: true) note = build(:note, system: true)
expect(note.editable?).to be_falsy expect(note.editable?).to be_falsy
end end
it "returns false" do
note = build(:note, is_award: true, note: "smiley")
expect(note.editable?).to be_falsy
end
end end
describe "cross_reference_not_visible_for?" do describe "cross_reference_not_visible_for?" do
...@@ -229,29 +207,6 @@ describe Note, models: true do ...@@ -229,29 +207,6 @@ describe Note, models: true do
end end
end end
describe "set_award!" do
let(:merge_request) { create :merge_request }
it "converts aliases to actual name" do
note = create(:note, note: ":+1:",
noteable: merge_request,
project: merge_request.project)
expect(note.reload.note).to eq("thumbsup")
end
it "is not an award emoji when comment is on a diff" do
note = create(:note_on_merge_request_diff, note: ":blowfish:",
noteable: merge_request,
project: merge_request.project,
line_code: "11d5d2e667e9da4f7f610f81d86c974b146b13bd_0_2")
note = note.reload
expect(note.note).to eq(":blowfish:")
expect(note.is_award?).to be_falsy
end
end
describe 'clear_blank_line_code!' do describe 'clear_blank_line_code!' do
it 'clears a blank line code before validation' do it 'clears a blank line code before validation' do
note = build(:note, line_code: ' ') note = build(:note, line_code: ' ')
......
...@@ -30,6 +30,7 @@ describe User, models: true do ...@@ -30,6 +30,7 @@ describe User, models: true do
it { is_expected.to have_one(:abuse_report) } it { is_expected.to have_one(:abuse_report) }
it { is_expected.to have_many(:spam_logs).dependent(:destroy) } it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) }
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
end end
describe 'validations' do describe 'validations' do
......
...@@ -249,7 +249,6 @@ describe API::API, api: true do ...@@ -249,7 +249,6 @@ describe API::API, api: true do
expect(json_response['milestone']).to be_a Hash expect(json_response['milestone']).to be_a Hash
expect(json_response['assignee']).to be_a Hash expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash expect(json_response['author']).to be_a Hash
expect(json_response['user_notes_count']).to be(1)
end end
it "should return a project issue by id" do it "should return a project issue by id" do
......
...@@ -138,7 +138,6 @@ describe API::API, api: true do ...@@ -138,7 +138,6 @@ describe API::API, api: true do
expect(json_response['work_in_progress']).to be_falsy expect(json_response['work_in_progress']).to be_falsy
expect(json_response['merge_when_build_succeeds']).to be_falsy expect(json_response['merge_when_build_succeeds']).to be_falsy
expect(json_response['merge_status']).to eq('can_be_merged') expect(json_response['merge_status']).to eq('can_be_merged')
expect(json_response['user_notes_count']).to be(2)
end end
it "should return merge_request" do it "should return merge_request" do
......
...@@ -39,6 +39,7 @@ describe Issues::MoveService, services: true do ...@@ -39,6 +39,7 @@ describe Issues::MoveService, services: true do
let!(:milestone2) do let!(:milestone2) do
create(:milestone, project_id: new_project.id, title: 'v9.0') create(:milestone, project_id: new_project.id, title: 'v9.0')
end end
let!(:award_emoji) { create(:award_emoji, awardable: old_issue) }
let!(:new_issue) { move_service.execute(old_issue, new_project) } let!(:new_issue) { move_service.execute(old_issue, new_project) }
end end
...@@ -115,6 +116,10 @@ describe Issues::MoveService, services: true do ...@@ -115,6 +116,10 @@ describe Issues::MoveService, services: true do
it 'preserves create time' do it 'preserves create time' do
expect(old_issue.created_at).to eq new_issue.created_at expect(old_issue.created_at).to eq new_issue.created_at
end end
it 'moves the award emoji' do
expect(old_issue.award_emoji.first.name).to eq new_issue.reload.award_emoji.first.name
end
end end
context 'issue with notes' do context 'issue with notes' do
......
...@@ -28,18 +28,16 @@ describe Notes::CreateService, services: true do ...@@ -28,18 +28,16 @@ describe Notes::CreateService, services: true do
project.team << [user, :master] project.team << [user, :master]
end end
it "creates emoji note" do it "creates an award emoji" do
opts = { opts = {
note: ':smile: ', note: ':smile: ',
noteable_type: 'Issue', noteable_type: 'Issue',
noteable_id: issue.id noteable_id: issue.id
} }
note = Notes::CreateService.new(project, user, opts).execute
@note = Notes::CreateService.new(project, user, opts).execute expect(note).to be_valid
expect(note.name).to eq('smile')
expect(@note).to be_valid
expect(@note.note).to eq('smile')
expect(@note.is_award).to be_truthy
end end
it "creates regular note if emoji name is invalid" do it "creates regular note if emoji name is invalid" do
...@@ -48,12 +46,22 @@ describe Notes::CreateService, services: true do ...@@ -48,12 +46,22 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue', noteable_type: 'Issue',
noteable_id: issue.id noteable_id: issue.id
} }
note = Notes::CreateService.new(project, user, opts).execute
@note = Notes::CreateService.new(project, user, opts).execute expect(note).to be_valid
expect(note.note).to eq(opts[:note])
end
it "normalizes the emoji name" do
opts = {
note: ':+1:',
noteable_type: 'Issue',
noteable_id: issue.id
}
expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user)
expect(@note).to be_valid Notes::CreateService.new(project, user, opts).execute
expect(@note.note).to eq(opts[:note])
expect(@note.is_award).to be_falsy
end end
end end
end end
...@@ -156,7 +156,6 @@ describe TodoService, services: true do ...@@ -156,7 +156,6 @@ describe TodoService, services: true do
let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) } let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) } let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) }
let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) } let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) }
let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') }
let(:system_note) { create(:system_note, project: project, noteable: issue) } let(:system_note) { create(:system_note, project: project, noteable: issue) }
it 'mark related pending todos to the noteable for the note author as done' do it 'mark related pending todos to the noteable for the note author as done' do
...@@ -169,13 +168,6 @@ describe TodoService, services: true do ...@@ -169,13 +168,6 @@ describe TodoService, services: true do
expect(second_todo.reload).to be_done expect(second_todo.reload).to be_done
end end
it 'mark related pending todos to the noteable for the award note author as done' do
service.new_note(award_note, john_doe)
expect(first_todo.reload).to be_done
expect(second_todo.reload).to be_done
end
it 'does not mark related pending todos it is a system note' do it 'does not mark related pending todos it is a system note' do
service.new_note(system_note, john_doe) service.new_note(system_note, john_doe)
...@@ -306,6 +298,15 @@ describe TodoService, services: true do ...@@ -306,6 +298,15 @@ describe TodoService, services: true do
end end
end end
describe '#new_award_emoji' do
it 'marks related pending todos to the target for the user as done' do
todo = create(:todo, user: john_doe, project: project, target: mr_assigned, author: author)
service.new_award_emoji(mr_assigned, john_doe)
expect(todo.reload).to be_done
end
end
describe '#merge_request_build_failed' do describe '#merge_request_build_failed' do
it 'creates a pending todo for the merge request author' do it 'creates a pending todo for the merge request author' do
service.merge_request_build_failed(mr_unassigned) service.merge_request_build_failed(mr_unassigned)
......
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