summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/awards_handler.coffee265
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee2
-rw-r--r--app/assets/javascripts/lib/emoji_aliases.js.coffee.erb9
-rw-r--r--app/assets/javascripts/notes.js.coffee6
-rw-r--r--app/assets/stylesheets/pages/awards.scss13
-rw-r--r--app/assets/stylesheets/pages/notes.scss41
-rw-r--r--app/controllers/concerns/toggle_award_emoji.rb20
-rw-r--r--app/controllers/projects/issues_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests_controller.rb6
-rw-r--r--app/controllers/projects/notes_controller.rb34
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/finders/notes_finder.rb4
-rw-r--r--app/helpers/issues_helper.rb14
-rw-r--r--app/models/award_emoji.rb35
-rw-r--r--app/models/concerns/awardable.rb81
-rw-r--r--app/models/concerns/issuable.rb44
-rw-r--r--app/models/note.rb52
-rw-r--r--app/models/user.rb1
-rw-r--r--app/services/notes/create_service.rb5
-rw-r--r--app/services/notes/post_process_service.rb2
-rw-r--r--app/services/notification_service.rb1
-rw-r--r--app/services/todo_service.rb8
-rw-r--r--app/services/toggle_award_emoji_service.rb21
-rw-r--r--app/views/award_emoji/_awards_block.html.haml15
-rw-r--r--app/views/emojis/index.html.haml4
-rw-r--r--app/views/projects/issues/_issue.html.haml16
-rw-r--r--app/views/projects/issues/show.html.haml6
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml16
-rw-r--r--app/views/projects/merge_requests/_show.html.haml4
-rw-r--r--app/views/votes/_votes_block.html.haml8
-rw-r--r--config/initializers/inflections.rb4
-rw-r--r--config/routes.rb7
-rw-r--r--db/migrate/20160416180807_add_award_emoji.rb15
-rw-r--r--db/migrate/20160416182152_convert_award_note_to_emoji_award.rb17
-rw-r--r--db/migrate/20160416190505_remove_note_is_award.rb5
-rw-r--r--db/schema.rb15
-rw-r--r--features/steps/project/issues/issues.rb10
-rw-r--r--features/steps/project/merge_requests.rb10
-rw-r--r--lib/api/entities.rb7
-rw-r--r--lib/award_emoji.rb84
-rw-r--r--lib/gitlab/award_emoji.rb82
-rw-r--r--spec/controllers/groups_controller_spec.rb12
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb14
-rw-r--r--spec/factories/award_emoji.rb18
-rw-r--r--spec/factories/notes.rb6
-rw-r--r--spec/features/issues_spec.rb2
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb4
-rw-r--r--spec/helpers/issues_helper_spec.rb11
-rw-r--r--spec/lib/gitlab/award_emoji_spec.rb (renamed from spec/lib/award_emoji_spec.rb)6
-rw-r--r--spec/models/award_emoji_spec.rb31
-rw-r--r--spec/models/concerns/awardable_spec.rb49
-rw-r--r--spec/models/concerns/issuable_spec.rb11
-rw-r--r--spec/models/note_spec.rb39
-rw-r--r--spec/models/user_spec.rb1
-rw-r--r--spec/services/notes/create_service_spec.rb20
-rw-r--r--spec/services/todo_service_spec.rb17
-rw-r--r--spec/services/toggle_award_emoji_service_spec.rb39
57 files changed, 812 insertions, 463 deletions
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
index bf95e06b4e5..c18c9984c1f 100644
--- a/app/assets/javascripts/awards_handler.coffee
+++ b/app/assets/javascripts/awards_handler.coffee
@@ -1,63 +1,108 @@
class @AwardsHandler
- constructor: (@getEmojisUrl, @postEmojiUrl, @noteableType, @noteableId, @unicodes) ->
- $('.js-add-award').on 'click', (event) =>
- event.stopPropagation()
- event.preventDefault()
+ constructor: ->
+ @aliases = gl.emoji.emojiAliases()
- @showEmojiMenu()
+ $(document)
+ .off "click", ".js-add-award"
+ .on "click", ".js-add-award", (e) =>
+ e.stopPropagation()
+ e.preventDefault()
- $('html').on 'click', (event) ->
- if !$(event.target).closest('.emoji-menu').length
- if $('.emoji-menu').is(':visible')
- $('.emoji-menu').removeClass 'is-visible'
+ @showEmojiMenu $(e.currentTarget)
- $('.awards')
- .off 'click'
- .on 'click', '.js-emoji-btn', @handleClick
+ $("html").on 'click', (e) ->
+ if !$(e.target).closest(".emoji-menu").length
+ if $(".emoji-menu").is(":visible")
+ $('.js-add-award.is-active').removeClass 'is-active'
+ $(".emoji-menu").removeClass "is-visible"
- @renderFrequentlyUsedBlock()
+ $(document)
+ .off "click", ".js-emoji-btn"
+ .on "click", ".js-emoji-btn", @handleClick.bind(@)
handleClick: (e) ->
e.preventDefault()
- emoji = $(this)
- .find('.icon')
- .data 'emoji'
+ $emojiBtn = $(e.currentTarget)
+ $addAwardBtn = $('.js-add-award.is-active')
+ $votesBlock = $($addAwardBtn.closest('.js-award-holder').data('target'))
- if emoji is 'thumbsup' and awardsHandler.didUserClickEmoji $(this), 'thumbsdown'
- awardsHandler.addAward 'thumbsdown'
+ if $addAwardBtn.length is 0
+ $votesBlock = $emojiBtn.closest('.js-awards-block')
+ else if $votesBlock.length is 0
+ $votesBlock = $addAwardBtn.closest('.js-awards-block')
- else if emoji is 'thumbsdown' and awardsHandler.didUserClickEmoji $(this), 'thumbsup'
- awardsHandler.addAward 'thumbsup'
+ @currentVoteBlock = $votesBlock
- awardsHandler.addAward emoji
+ awardUrl = $votesBlock.data 'award-url'
+ emoji = $emojiBtn
+ .find(".icon")
+ .data "emoji"
- $(this).trigger 'blur'
+ if emoji is "thumbsup" and @didUserClickEmoji $emojiBtn, "thumbsdown"
+ @addAward awardUrl, "thumbsdown"
- didUserClickEmoji: (that, emoji) ->
- if $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title')
- $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title').indexOf('me') > -1
+ else if emoji is "thumbsdown" and @didUserClickEmoji $emojiBtn, "thumbsup"
+ @addAward awardUrl, "thumbsup"
- showEmojiMenu: ->
- if $('.emoji-menu').length
- if $('.emoji-menu').is '.is-visible'
- $('.emoji-menu').removeClass 'is-visible'
- $('#emoji_search').blur()
+ @addAward awardUrl, emoji
+
+ didUserClickEmoji: (emojiBtn, emoji) ->
+ if emojiBtn.siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title")
+ emojiBtn.siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title").indexOf('me') > -1
+
+ showEmojiMenu: ($addBtn) ->
+ $menu = $('.emoji-menu')
+ if $menu.length
+ $holder = $addBtn.closest('.js-award-holder')
+
+ if $menu.is ".is-visible"
+ $addBtn.removeClass "is-active"
+ $menu.removeClass "is-visible"
+ $("#emoji_search").blur()
else
- $('.emoji-menu').addClass 'is-visible'
- $('#emoji_search').focus()
+ $(".emoji-menu").addClass "is-visible"
+ $addBtn.addClass "is-active"
+ @positionMenu($menu, $addBtn)
+
+ $menu.addClass "is-visible"
+ $("#emoji_search").focus()
else
- $('.js-add-award').addClass 'is-loading'
- $.get @getEmojisUrl, (response) =>
- $('.js-add-award').removeClass 'is-loading'
- $('.js-award-holder').append response
+ $addBtn.addClass "is-loading is-active"
+ $.get $addBtn.data('award-menu-url'), (response) =>
+ $addBtn.removeClass "is-loading"
+ $('body').append response
+
+ $menu = $(".emoji-menu")
+
+ @positionMenu($menu, $addBtn)
+
+ @renderFrequentlyUsedBlock()
setTimeout =>
- $('.emoji-menu').addClass 'is-visible'
- $('#emoji_search').focus()
+ $menu.addClass "is-visible"
+ $("#emoji_search").focus()
@setupSearch()
, 200
- addAward: (emoji) ->
- @postEmoji emoji, =>
+ 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) ->
+ emoji = @normilizeEmojiName(emoji)
+ @postEmoji awardUrl, emoji, =>
@addAwardToEmojiBar(emoji)
$('.emoji-menu').removeClass 'is-visible'
@@ -65,59 +110,62 @@ class @AwardsHandler
addAwardToEmojiBar: (emoji) ->
@addEmojiToFrequentlyUsedList(emoji)
- if @exist(emoji)
- if @isActive(emoji)
- @decrementCounter(emoji)
+ emoji = @normilizeEmojiName(emoji)
+ $emojiBtn = @findEmojiIcon(emoji).parent()
+
+ if $emojiBtn.length > 0
+ if @isActive($emojiBtn)
+ @decrementCounter($emojiBtn, emoji)
else
- counter = @findEmojiIcon(emoji).siblings('.js-counter')
- counter.text(parseInt(counter.text()) + 1)
- counter.parent().addClass('active')
- @addMeToAuthorList(emoji)
+ $counter = $emojiBtn.find('.js-counter')
+ $counter.text(parseInt($counter.text()) + 1)
+ $emojiBtn.addClass("active")
+ @addMeToUserList(emoji)
else
@createEmoji(emoji)
- exist: (emoji) ->
- @findEmojiIcon(emoji).length > 0
-
- isActive: (emoji) ->
- @findEmojiIcon(emoji).parent().hasClass('active')
-
- decrementCounter: (emoji) ->
- counter = @findEmojiIcon(emoji).siblings('.js-counter')
- emojiIcon = counter.parent()
- if parseInt(counter.text()) > 1
- counter.text(parseInt(counter.text()) - 1)
- emojiIcon.removeClass('active')
- @removeMeFromAuthorList(emoji)
- else if emoji == 'thumbsup' || emoji == 'thumbsdown'
- emojiIcon.tooltip('destroy')
- counter.text(0)
- emojiIcon.removeClass('active')
- @removeMeFromAuthorList(emoji)
+ isActive: ($emojiBtn) ->
+ $emojiBtn.hasClass("active")
+
+ decrementCounter: ($emojiBtn, emoji) ->
+ $awardsBlock = $emojiBtn.closest('.js-awards-block')
+ isntNoteBody = $emojiBtn.closest('.note-body').length is 0
+ counter = $('.js-counter', $emojiBtn)
+ counterNumber = parseInt(counter.text())
+
+ 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
- emojiIcon.tooltip('destroy')
- emojiIcon.remove()
-
- removeMeFromAuthorList: (emoji) ->
- awardBlock = @findEmojiIcon(emoji).parent()
- authors = awardBlock
- .attr('data-original-title')
- .split(', ')
- authors.splice(authors.indexOf('me'),1)
- awardBlock
- .closest('.js-emoji-btn')
- .attr('data-original-title', authors.join(', '))
- @resetTooltip(awardBlock)
-
- addMeToAuthorList: (emoji) ->
- awardBlock = @findEmojiIcon(emoji).parent()
- origTitle = awardBlock.attr('data-original-title').trim()
- authors = []
+ $emojiBtn.tooltip("destroy")
+ $emojiBtn.remove()
+
+ $emojiBtn.removeClass("active")
+
+ removeMeFromUserList: ($emojiBtn, emoji) ->
+ award_block = $emojiBtn
+ authors = award_block
+ .attr("data-original-title")
+ .split(", ")
+ authors.splice(authors.indexOf("me"), 1)
+ award_block
+ .closest(".js-emoji-btn")
+ .attr("data-original-title", authors.join(", "))
+ @resetTooltip(award_block)
+
+ addMeToUserList: (emoji) ->
+ award_block = @findEmojiIcon(emoji).parent()
+ origTitle = award_block.attr("data-original-title").trim()
+ users = []
if origTitle
- authors = origTitle.split(', ')
- authors.push('me')
- awardBlock.attr('data-original-title', authors.join(', '))
- @resetTooltip(awardBlock)
+ users = origTitle.split(', ')
+ users.push("me")
+ award_block.attr("data-original-title", users.join(", "))
+ @resetTooltip(award_block)
resetTooltip: (award) ->
award.tooltip('destroy')
@@ -127,24 +175,24 @@ class @AwardsHandler
award.tooltip()
), 200
-
createEmoji: (emoji) ->
emojiCssClass = @resolveNameToCssClass(emoji)
- nodes = []
- nodes.push(
- "<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>",
- "<span class='award-control-text js-counter'>1</span>",
- "</button>"
- )
-
- $(nodes.join("\n"))
- .insertBefore('.js-award-holder')
- .find('.emoji-icon')
- .data('emoji', emoji)
+ buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'>
+ <div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>
+ <span class='award-control-text js-counter'>1</span>
+ </button>"
+
+ emoji_node = $(buttonHtml)
+ .insertBefore(".js-awards-block-current .js-award-holder:not(.js-award-action-btn)")
+ .find(".emoji-icon")
+ .data("emoji", emoji)
$('.award-control').tooltip()
+ $currentBlock = $('.js-awards-block-current')
+ if $currentBlock.is('.hidden')
+ $currentBlock.removeClass 'hidden'
+
resolveNameToCssClass: (emoji) ->
emojiIcon = $(".emoji-menu-content [data-emoji='#{emoji}']")
@@ -156,17 +204,13 @@ class @AwardsHandler
"emoji-#{unicodeName}"
- postEmoji: (emoji, callback) ->
- $.post @postEmojiUrl, { note: {
- note: ":#{emoji}:"
- noteable_type: @noteableType
- noteable_id: @noteableId
- }},(data) ->
+ postEmoji: (awardUrl, emoji, callback) ->
+ $.post awardUrl, { name: emoji }, (data) ->
if data.ok
callback.call()
findEmojiIcon: (emoji) ->
- $(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
+ @currentVoteBlock.find(".js-emoji-btn [data-emoji='#{emoji}']")
scrollToAwards: ->
$('body, html').animate({
@@ -186,11 +230,10 @@ class @AwardsHandler
if $.cookie('frequently_used_emojis')
frequentlyUsedEmojis = @getFrequentlyUsedEmojis()
- ul = $('<ul>')
+ ul = $("<ul class='clearfix emoji-menu-list'>")
- for emoji in frequentlyUsedEmojis
- do (emoji) ->
- $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
+ for emoji in frequently_used_emojis
+ $(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul)
$('input.emoji-search').after(ul).after($('<h5>').text('Frequently used'))
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index f91aa3c5ad7..e00ca2984b9 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -23,6 +23,7 @@ class Dispatcher
new Issue()
shortcut_handler = new ShortcutsIssuable()
new ZenMode()
+ awards_handler = new AwardsHandler()
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone()
when 'dashboard:todos:index'
@@ -53,6 +54,7 @@ class Dispatcher
new Diff()
shortcut_handler = new ShortcutsIssuable(true)
new ZenMode()
+ awards_handler = new AwardsHandler()
when "projects:merge_requests:diffs"
new Diff()
new ZenMode()
diff --git a/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb
new file mode 100644
index 00000000000..e005ab3684e
--- /dev/null
+++ b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb
@@ -0,0 +1,9 @@
+((w) ->
+
+ w.gl ?= {}
+ w.gl.emoji ?= {}
+
+ w.gl.emoji.emojiAliases = ->
+ JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
+
+) window \ No newline at end of file
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index efb3e8e2198..e8a92b8012d 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -162,13 +162,13 @@ class @Notes
renderNote: (note) ->
unless note.valid
if note.award
- flash = new Flash('You have already used this award emoji!', 'alert')
+ flash = new Flash('You have already awarded this emoji, it has been removed', 'alert')
flash.pinTo('.header-content')
return
if note.award
- awardsHandler.addAwardToEmojiBar(note.note)
- awardsHandler.scrollToAwards()
+ awards_handler.addAwardToEmojiBar(note.name)
+ awards_handler.scrollToAwards()
# render note if it not present in loaded list
# or skip if rendered
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
index 37bf38fa65d..07d40f40556 100644
--- a/app/assets/stylesheets/pages/awards.scss
+++ b/app/assets/stylesheets/pages/awards.scss
@@ -1,6 +1,4 @@
.awards {
- line-height: 34px;
-
.emoji-icon {
width: 20px;
height: 20px;
@@ -9,8 +7,6 @@
.emoji-menu {
position: absolute;
- top: 100%;
- left: 0;
margin-top: 3px;
z-index: 1000;
min-width: 160px;
@@ -23,7 +19,12 @@
opacity: 0;
transform: scale(.2);
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 {
pointer-events: all;
@@ -107,7 +108,7 @@
}
&.is-loading {
- .award-control-icon {
+ .award-control-icon-normal {
display: none;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 624c8249f7e..8ad47a50541 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -63,7 +63,8 @@ ul.notes {
&.is-editting {
.note-header,
.note-text,
- .edited-text {
+ .edited-text,
+ .note-awards {
display: none;
}
@@ -73,8 +74,6 @@ ul.notes {
}
.note-body {
- overflow: auto;
-
.note-text {
overflow: auto;
word-wrap: break-word;
@@ -324,6 +323,42 @@ ul.notes {
}
}
+.note-award-control {
+ display: block;
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ }
+
+ .award-control-icon-loading {
+ display: none;
+ }
+
+ &.is-loading {
+ .award-control-icon-normal {
+ display: none;
+ }
+
+ .award-control-icon-loading {
+ display: block;
+ }
+ }
+}
+
+.note-awards {
+ .awards {
+ padding-top: 10px;
+ }
+
+ .award-control {
+ padding-top: 2px;
+ padding-bottom: 2px;
+ color: #8f8f8f;
+ font-size: 13px;
+ }
+}
+
.disabled-comment {
margin-left: -$gl-padding-top;
margin-right: -$gl-padding-top;
diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb
new file mode 100644
index 00000000000..9cd522d1c30
--- /dev/null
+++ b/app/controllers/concerns/toggle_award_emoji.rb
@@ -0,0 +1,20 @@
+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)
+
+ render json: { ok: true }
+ end
+
+ private
+
+ def awardable
+ raise NotImplementedError
+ end
+end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 016f5dd0005..5f3745d94b9 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -1,6 +1,7 @@
class Projects::IssuesController < Projects::ApplicationController
include ToggleSubscriptionAction
include IssuableActions
+ include ToggleAwardEmoji
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
@@ -62,7 +63,7 @@ class Projects::IssuesController < Projects::ApplicationController
def show
@note = @project.notes.new(noteable: @issue)
- @notes = @issue.notes.nonawards.with_associations.fresh
+ @notes = @issue.notes.with_associations.fresh
@noteable = @issue
respond_to do |format|
@@ -169,6 +170,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
+ alias_method :awardable, :issue
def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue)
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 9c147b3689e..164c035e8bc 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
include ToggleSubscriptionAction
include DiffHelper
include IssuableActions
+ include ToggleAwardEmoji
before_action :module_enabled
before_action :merge_request, only: [
@@ -195,7 +196,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active?
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
- .execute(@merge_request)
+ .execute(@merge_request)
@status = :merge_when_build_succeeds
else
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@@ -264,6 +265,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
alias_method :subscribable_resource, :merge_request
alias_method :issuable, :merge_request
+ alias_method :awardable, :merge_request
def closes_issues
@closes_issues ||= @merge_request.closes_issues
@@ -299,7 +301,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_show_vars
# Build a note object for comment form
@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 = Note.discussions_from_notes(@notes)
@noteable = @merge_request
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 707a0d0e5c6..eb5137fe999 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -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, :award_toggle]
+ before_action :find_current_user_notes, only: [:index]
def index
current_fetched_at = Time.now.to_i
@@ -22,8 +22,10 @@ class Projects::NotesController < Projects::ApplicationController
def create
@note = Notes::CreateService.new(project, current_user, note_params).execute
+ @note = @note.is_a?(AwardEmoji) ? @note.to_note_json : note_json(@note)
+
respond_to do |format|
- format.json { render json: note_json(@note) }
+ format.json { render json: @note }
format.html { redirect_back_or_default }
end
end
@@ -56,30 +58,6 @@ class Projects::NotesController < Projects::ApplicationController
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
def note
@@ -137,7 +115,7 @@ class Projects::NotesController < Projects::ApplicationController
id: note.id,
discussion_id: note.discussion_id,
html: note_to_html(note),
- award: note.is_award,
+ award: false,
note: note.note,
discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
@@ -145,7 +123,7 @@ class Projects::NotesController < Projects::ApplicationController
else
{
valid: false,
- award: note.is_award,
+ award: false,
errors: note.errors
}
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 3768efe142a..85a987c2cb2 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -145,7 +145,7 @@ class ProjectsController < Projects::ApplicationController
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = {
- emojis: AwardEmoji.urls,
+ emojis: Gitlab::AwardEmoji.urls,
issues: autocomplete.issues,
mergerequests: autocomplete.merge_requests,
members: participants
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index fa4c635f55c..ab252821b52 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -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.nonawards.inc_author
+ project.issues.find(target_id).notes.inc_author
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"
project.snippets.find(target_id).notes
else
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 198d39455d7..0ea712c8266 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -162,16 +162,14 @@ module IssuesHelper
end
end
- def emoji_author_list(notes, current_user)
- list = notes.map do |note|
- note.author == current_user ? "me" : note.author.name
- end
-
- list.join(", ")
+ def award_user_list(awards, current_user)
+ awards.map do |award|
+ award.user == current_user ? 'me' : award.user.name
+ end.join(', ')
end
- def note_active_class(notes, current_user)
- if current_user && notes.pluck(:author_id).include?(current_user.id)
+ def award_active_class(awards, current_user)
+ if current_user && awards.find { |a| a.user_id == current_user.id }
"active"
else
""
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
new file mode 100644
index 00000000000..44a9b55a8a6
--- /dev/null
+++ b/app/models/award_emoji.rb
@@ -0,0 +1,35 @@
+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
+
+ def to_note_json
+ {
+ valid: valid?,
+ award: true,
+ id: id,
+ name: name
+ }
+ end
+end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
new file mode 100644
index 00000000000..b4e3e9eb3dd
--- /dev/null
+++ b/app/models/concerns/awardable.rb
@@ -0,0 +1,81 @@
+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] ||= AwardEmoji.none
+ awards[AwardEmoji::DOWNVOTE_NAME] ||= AwardEmoji.none
+ end
+
+ awards
+ end
+
+ def downvotes
+ award_emoji.where(name: AwardEmoji::DOWNVOTE_NAME).count
+ end
+
+ def upvotes
+ award_emoji.where(name: AwardEmoji::UPVOTE_NAME).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
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 7a3db742030..9b77b88ca80 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -10,6 +10,7 @@ module Issuable
include Mentionable
include Subscribable
include StripAttribute
+ include Awardable
included do
belongs_to :author, class_name: "User"
@@ -99,37 +100,6 @@ module Issuable
order_by(method)
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)
- if title.is_a?(Array) && title.count > 1
- joins(:labels).where(labels: { title: title }).group('issues.id').having("count(distinct labels.title) = #{title.count}")
- else
- joins(:labels).where(labels: { title: title })
- end
- end
end
def today?
@@ -152,18 +122,6 @@ module Issuable
opened? || reopened?
end
- def downvotes
- notes.awards.where(note: "thumbsdown").count
- end
-
- def upvotes
- notes.awards.where(note: "thumbsup").count
- end
-
- def user_notes_count
- notes.user.count
- end
-
def subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
diff --git a/app/models/note.rb b/app/models/note.rb
index f26aa1bf63f..0b038edb47b 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -21,12 +21,9 @@ class Note < ActiveRecord::Base
delegate :name, to: :project, prefix: true
delegate :name, :email, to: :author, prefix: true
- before_validation :set_award!
before_validation :clear_blank_line_code!
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 }
validates :line_code, line_code: true, allow_blank: true
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
@@ -38,8 +35,6 @@ class Note < ActiveRecord::Base
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,19 +92,6 @@ class Note < ActiveRecord::Base
where(table[:note].matches(pattern))
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
def cross_reference?
@@ -325,37 +307,25 @@ class Note < ActiveRecord::Base
Event.reset_event_cache_for(self)
end
- def downvote?
- is_award && note == "thumbsdown"
- end
-
- def upvote?
- is_award && note == "thumbsup"
+ def system?
+ read_attribute(:system)
end
def editable?
- !system? && !is_award
+ !system?
end
def cross_reference_not_visible_for?(user)
cross_reference? && referenced_mentionables(user).empty?
end
- # Checks if note is an award added as a comment
- #
- # 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
+ def award_emoji?
+ award_emoji_supported? && contains_emoji_only?
end
- private
+ def create_award_emoji
+ self.noteable.award_emoji(award_emoji_name, author)
+ end
def clear_blank_line_code!
self.line_code = nil if self.line_code.blank?
@@ -367,8 +337,8 @@ class Note < ActiveRecord::Base
diffs.find { |d| d.new_path == self.diff.new_path }
end
- def awards_supported?
- (for_issue? || for_merge_request?) && !for_diff_line?
+ def award_emoji_supported?
+ noteable.is_a?(Awardable) && !for_diff_line?
end
def contains_emoji_only?
@@ -377,6 +347,6 @@ class Note < ActiveRecord::Base
def award_emoji_name
original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
- AwardEmoji.normilize_emoji_name(original_name)
+ Gitlab::AwardEmoji.normilize_emoji_name(original_name)
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 1e4814641d1..37179c10788 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -79,6 +79,7 @@ class User < ActiveRecord::Base
has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy
+ has_many :award_emoji, as: :awardable, dependent: :destroy
#
# Validations
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 01586994813..da2a774b70d 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -5,6 +5,11 @@ module Notes
note.author = current_user
note.system = false
+ if note.award_emoji?
+ return ToggleAwardEmojiService.new(project, current_user, params).
+ execute(note.noteable, note.note)
+ end
+
return unless valid_project?(note)
if note.save
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index e818f58d13c..c1bf46bdfb3 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -8,7 +8,7 @@ module Notes
def execute
# 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)
@note.create_cross_references!
execute_note_hooks
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 42ec1ac9e1a..703636658b7 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -131,7 +131,6 @@ 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
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 42c5bca90fd..da1b77c0f9e 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -98,6 +98,14 @@ class TodoService
handle_note(note, current_user)
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:
#
# * mark all pending todos related to the target for the current user as done
diff --git a/app/services/toggle_award_emoji_service.rb b/app/services/toggle_award_emoji_service.rb
new file mode 100644
index 00000000000..b77b4e79bf2
--- /dev/null
+++ b/app/services/toggle_award_emoji_service.rb
@@ -0,0 +1,21 @@
+require_relative 'base_service'
+
+class ToggleAwardEmojiService < BaseService
+ # For an award emoji being posted we should:
+ # - Mark the TODO as done for this issuable (skip on snippets)
+ # - Save the award emoji
+ def execute(awardable, emoji)
+ todo_service.new_award_emoji(awardable, current_user)
+
+ # Needed if its posted as a note containing only :+1:
+ emoji = award_emoji_name(emoji) if emoji.start_with? ':'
+ awardable.toggle_award_emoji(emoji, current_user)
+ end
+
+ private
+
+ def award_emoji_name(emoji)
+ original_name = emoji.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
+ Gitlab::AwardEmoji.normalize_emoji_name(original_name)
+ end
+end
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
new file mode 100644
index 00000000000..b57c9afcbd2
--- /dev/null
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -0,0 +1,15 @@
+- grouped_emojis = awardable.grouped_awards(inline)
+.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.size == 0), 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", original_title: award_user_list(awards, current_user)} }
+ = emoji_icon(emoji)
+ %span.award-control-text.js-counter
+ = awards.count
+
+ - if current_user
+ .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
diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml
index 3443a8e2307..97401a2e618 100644
--- a/app/views/emojis/index.html.haml
+++ b/app/views/emojis/index.html.haml
@@ -1,9 +1,9 @@
.emoji-menu
.emoji-menu-content
= 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
- = AwardEmoji::CATEGORIES[category]
+ = Gitlab::AwardEmoji::CATEGORIES[category]
%ul.clearfix.emoji-menu-list
- emojis.each do |emoji|
%li.pull-left.text-center.emoji-menu-list-item
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 5cf70ea3bb7..57ad2ec1852 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -27,11 +27,17 @@
= icon('thumbs-down')
= downvotes
- - note_count = issue.notes.user.nonawards.count
- %li
- = link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do
- = icon('comments')
- = note_count
+ - note_count = issue.notes.user.count
+ - if note_count > 0
+ %li
+ = link_to issue_path(issue) + "#notes" do
+ = icon('comments')
+ = note_count
+ - else
+ %li
+ = link_to issue_path(issue) + "#notes", class: "issue-no-comments" do
+ = icon('comments')
+ = note_count
.issue-info
#{issue.to_reference} &middot;
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index bde80bbb54b..6623028c02e 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -69,9 +69,9 @@
#related-branches{ data: { url: related_branches_namespace_project_issue_url(@project.namespace, @project, @issue) } }
// This element is filled in using JavaScript.
- .content-block.content-block-small
- = render 'new_branch'
- = render 'votes/votes_block', votable: @issue
+ .content-block.content-block-small
+ = render 'new_branch'
+ = render 'award_emoji/awards_block', awardable: @issue, inline: true
%section.issuable-discussion
= render 'projects/issues/discussion'
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 73c6a95f5ca..391193eed6c 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -35,11 +35,17 @@
= icon('thumbs-down')
= downvotes
- - note_count = merge_request.mr_and_commit_notes.user.nonawards.count
- %li
- = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do
- = icon('comments')
- = note_count
+ - note_count = merge_request.mr_and_commit_notes.user.count
+ - if note_count > 0
+ %li
+ = link_to merge_request_path(merge_request) + "#notes" do
+ = icon('comments')
+ = note_count
+ - else
+ %li
+ = link_to merge_request_path(merge_request) + "#notes", class: "merge-request-no-comments" do
+ = icon('comments')
+ = note_count
.merge-request-info
#{merge_request.to_reference} &middot;
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 290753d57c6..23525318400 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -50,7 +50,7 @@
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
Discussion
- %span.badge= @merge_request.mr_and_commit_notes.user.nonawards.count
+ %span.badge= @merge_request.mr_and_commit_notes.user.count
%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
Commits
@@ -68,7 +68,7 @@
.tab-content
#notes.notes.tab-pane.voting_notes
.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
%section.col-md-12
diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml
index 4beb8746444..ab8bb949862 100644
--- a/app/views/votes/_votes_block.html.haml
+++ b/app/views/votes/_votes_block.html.haml
@@ -1,9 +1,9 @@
-.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)}}
+.awards.votes-block{data: { toggle_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) }}
+ - awards_sort(awardable.grouped_awards).each do |emoji, awards|
+ %button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(awards, current_user)), data: {placement: "top", original_title: emoji_author_list(awards, current_user)}}
= emoji_icon(emoji, sprite: false)
%span.award-control-text.js-counter
- = notes.count
+ = awards.count
- if current_user
%div.award-menu-holder.js-award-holder
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index 9e8b0131f8f..3d1a41a4652 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -8,3 +8,7 @@
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
# end
+#
+ActiveSupport::Inflector.inflections do |inflect|
+ inflect.uncountable %w(award_emoji)
+end
diff --git a/config/routes.rb b/config/routes.rb
index dafecc94648..8deb224cde9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -643,6 +643,7 @@ Rails.application.routes.draw do
post :cancel_merge_when_build_succeeds
get :ci_status
post :toggle_subscription
+ post :toggle_award_emoji
post :remove_wip
end
@@ -708,6 +709,7 @@ Rails.application.routes.draw do
resources :issues, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
+ post :toggle_award_emoji
get :referenced_merge_requests
get :related_branches
get :can_create_branch
@@ -737,10 +739,7 @@ Rails.application.routes.draw do
resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
member do
delete :delete_attachment
- end
-
- collection do
- post :award_toggle
+ post :toggle_award_emoji
end
end
diff --git a/db/migrate/20160416180807_add_award_emoji.rb b/db/migrate/20160416180807_add_award_emoji.rb
new file mode 100644
index 00000000000..3177b86a133
--- /dev/null
+++ b/db/migrate/20160416180807_add_award_emoji.rb
@@ -0,0 +1,15 @@
+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
+ add_index :award_emoji, :awardable_id
+ end
+end
diff --git a/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb
new file mode 100644
index 00000000000..76f4a3aa6ae
--- /dev/null
+++ b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb
@@ -0,0 +1,17 @@
+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)"
+ end
+
+ def down
+ execute <<-SQL
+ INSERT INTO notes (noteable_type, noteable_id, author_id, note, created_at, updated_at, is_award)
+ (SELECT awardable_type, awardable_id, user_id, name, created_at, updated_at, TRUE
+ FROM award_emoji
+ WHERE awardable_type IN ('Issue', 'MergeRequest')
+ )
+ SQL
+ end
+ end
+end
diff --git a/db/migrate/20160416190505_remove_note_is_award.rb b/db/migrate/20160416190505_remove_note_is_award.rb
new file mode 100644
index 00000000000..da16372a297
--- /dev/null
+++ b/db/migrate/20160416190505_remove_note_is_award.rb
@@ -0,0 +1,5 @@
+class RemoveNoteIsAward < ActiveRecord::Migration
+ def change
+ remove_column :notes, :is_award, :boolean
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 71d953afe30..6db1c8acfa2 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -96,6 +96,19 @@ ActiveRecord::Schema.define(version: 20160508194200) 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", ["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_id"], name: "index_award_emoji_on_awardable_id", using: :btree
+ add_index "award_emoji", ["awardable_type"], name: "index_award_emoji_on_awardable_type", 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|
t.text "message", null: false
t.datetime "starts_at"
@@ -633,14 +646,12 @@ ActiveRecord::Schema.define(version: 20160508194200) 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", ["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
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index fc12843ea5c..78ddaee8771 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -191,15 +191,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'issue "Release 0.4" have 2 upvotes and 1 downvote' do
- issue = Issue.find_by(title: 'Release 0.4')
- create_list(:upvote_note, 2, project: project, noteable: issue)
- create(:downvote_note, project: project, noteable: issue)
+ awardable = Issue.find_by(title: 'Release 0.4')
+ create_list(:upvote, 2, project: project, awardable: awardable)
+ create(:downvote, project: project, awardable: awardable)
end
step 'issue "Tweet control" have 1 upvote and 2 downvotes' do
issue = Issue.find_by(title: 'Tweet control')
- create(:upvote_note, project: project, noteable: issue)
- create_list(:downvote_note, 2, project: project, noteable: issue)
+ create(:upvote, project: project, noteable: issue)
+ create_list(:downvote, 2, project: project, noteable: issue)
end
step 'The list should be sorted by "Least popular"' do
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 3b1a00f628a..6a9a079592c 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -179,14 +179,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do
merge_request = MergeRequest.find_by(title: 'Bug NS-04')
- create_list(:upvote_note, 2, project: project, noteable: merge_request)
- create(:downvote_note, project: project, noteable: merge_request)
+ create_list(:upvote, 2, project: project, awardable: merge_request)
+ create(:downvote, project: project, awardable: merge_request)
end
step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do
- merge_request = MergeRequest.find_by(title: 'Bug NS-06')
- create(:upvote_note, project: project, noteable: merge_request)
- create_list(:downvote_note, 2, project: project, noteable: merge_request)
+ awardable = MergeRequest.find_by(title: 'Bug NS-06')
+ create(:upvote, project: project, awardable: awardable)
+ create_list(:downvote, 2, project: project, awardable: awardable)
end
step 'The list should be sorted by "Least popular"' do
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 2870a6a40ef..f91ca9d5805 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -170,6 +170,7 @@ module API
expose :label_names, as: :labels
expose :milestone, using: Entities::Milestone
expose :assignee, :author, using: Entities::UserBasic
+
expose :subscribed do |issue, options|
issue.subscribed?(options[:current_user])
end
@@ -178,7 +179,7 @@ module API
class MergeRequest < ProjectEntity
expose :target_branch, :source_branch
- expose :upvotes, :downvotes
+ expose :upvotes, :downvotes
expose :author, :assignee, using: Entities::UserBasic
expose :source_project_id, :target_project_id
expose :label_names, as: :labels
@@ -216,8 +217,8 @@ module API
expose :system?, as: :system
expose :noteable_id, :noteable_type
# upvote? and downvote? are deprecated, always return false
- expose :upvote?, as: :upvote
- expose :downvote?, as: :downvote
+ expose(:upvote?) { |note| false }
+ expose(:downvote?) { |note| false }
end
class MRNote < Grape::Entity
diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb
deleted file mode 100644
index b1aecc2e671..00000000000
--- a/lib/award_emoji.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-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
diff --git a/lib/gitlab/award_emoji.rb b/lib/gitlab/award_emoji.rb
new file mode 100644
index 00000000000..0ae220a86bc
--- /dev/null
+++ b/lib/gitlab/award_emoji.rb
@@ -0,0 +1,82 @@
+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
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 465531b2b36..82b25702172 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -31,9 +31,9 @@ describe GroupsController do
let(:issue_2) { create(:issue, project: project) }
before do
- create_list(:upvote_note, 3, project: project, noteable: issue_2)
- create_list(:upvote_note, 2, project: project, noteable: issue_1)
- create_list(:downvote_note, 2, project: project, noteable: issue_2)
+ create_list(:award_emoji, 3, awardable: issue_2)
+ create_list(:award_emoji, 2, awardable: issue_1)
+ create_list(:award_emoji, 2, awardable: issue_2, name: "thumbsdown")
sign_in(user)
end
@@ -56,9 +56,9 @@ describe GroupsController do
let(:merge_request_2) { create(:merge_request, :simple, source_project: project) }
before do
- create_list(:upvote_note, 3, project: project, noteable: merge_request_2)
- create_list(:upvote_note, 2, project: project, noteable: merge_request_1)
- create_list(:downvote_note, 2, project: project, noteable: merge_request_2)
+ create_list(:award_emoji, 3, awardable: merge_request_2)
+ create_list(:award_emoji, 2, awardable: merge_request_1)
+ create_list(:award_emoji, 2, awardable: merge_request_2, name: "thumbsdown")
sign_in(user)
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 2b2ad3b9412..30d296fdad0 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -250,4 +250,18 @@ describe Projects::IssuesController do
end
end
end
+
+ describe 'POST #toggle_award_emoji' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it "yields status code 200" do
+ post(:toggle_award_emoji, namespace_id: project.namespace.path,
+ project_id: project.path, id: issue.iid, name: "thumbsup")
+
+ expect(response.status).to eq(200)
+ end
+ end
end
diff --git a/spec/factories/award_emoji.rb b/spec/factories/award_emoji.rb
new file mode 100644
index 00000000000..b09f8b0bc74
--- /dev/null
+++ b/spec/factories/award_emoji.rb
@@ -0,0 +1,18 @@
+FactoryGirl.define do
+ factory :award_emoji do
+ name "thumbsup"
+ user
+ awardable factory: :issue
+
+ trait :thumbs_up
+ trait :upvote
+
+ trait :thumbs_down do
+ name "thumbsdown"
+ end
+
+ trait :downvote do
+ name "thumbsdown"
+ end
+ end
+end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 840b13196a6..e65204cbe7b 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -15,8 +15,6 @@ FactoryGirl.define do
factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff]
factory :note_on_project_snippet, traits: [:on_project_snippet]
factory :system_note, traits: [:system]
- factory :downvote_note, traits: [:award, :downvote]
- factory :upvote_note, traits: [:award, :upvote]
trait :on_commit do
project
@@ -48,10 +46,6 @@ FactoryGirl.define do
system true
end
- trait :award do
- is_award true
- end
-
trait :downvote do
note "thumbsdown"
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index d5755c293c5..dfb554c05ca 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -67,7 +67,7 @@ describe 'Issues', feature: true do
describe 'Issue info' do
it 'excludes award_emoji from comment count' do
issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar')
- create(:upvote_note, noteable: issue)
+ create(:award_emoji, awardable: issue)
visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index 389812ff7e1..9e2bdd7f5bb 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -8,7 +8,7 @@ describe 'Comments', 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)
+ create(:award_emoji, awardable: merge_request, project: project)
login_as :admin
visit namespace_project_merge_requests_path(project.namespace, project)
@@ -146,7 +146,7 @@ describe 'Comments', feature: true do
describe 'comment info' do
it 'excludes award_emoji from comment count' do
- create(:upvote_note, noteable: merge_request, project: project)
+ create(:award_emoji, awardable: merge_request, project: project)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index bffe2c18b6f..eae61a54dfc 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -163,18 +163,15 @@ describe IssuesHelper do
it { is_expected.to eq("!1, !2, or !3") }
end
- describe "note_active_class" do
- before do
- @note = create :note
- @note1 = create :note
- end
+ describe '#award_active_class' do
+ let!(:upvote) { create(:award_emoji) }
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
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
diff --git a/spec/lib/award_emoji_spec.rb b/spec/lib/gitlab/award_emoji_spec.rb
index 88c22912950..4e6c04a11b9 100644
--- a/spec/lib/award_emoji_spec.rb
+++ b/spec/lib/gitlab/award_emoji_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
-describe AwardEmoji do
+describe Gitlab::AwardEmoji do
describe '.urls' do
- subject { AwardEmoji.urls }
+ subject { Gitlab::AwardEmoji.urls }
it { is_expected.to be_an_instance_of(Array) }
it { is_expected.to_not be_empty }
@@ -19,7 +19,7 @@ describe AwardEmoji do
describe '.emoji_by_category' 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
end
end
diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb
new file mode 100644
index 00000000000..fd3712b7d43
--- /dev/null
+++ b/spec/models/award_emoji_spec.rb
@@ -0,0 +1,31 @@
+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) }
+ it { is_expected.to validate_presence_of(:awardable) }
+
+ # 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 = AwardEmoji.new(user: user, awardable: issue, name: "thumbsup")
+
+ expect(new_award).not_to be_valid
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
new file mode 100644
index 00000000000..6851d068367
--- /dev/null
+++ b/spec/models/concerns/awardable_spec.rb
@@ -0,0 +1,49 @@
+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
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 4a4cd093435..568bf4c9324 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -12,6 +12,10 @@ describe Issue, "Issuable" do
it { is_expected.to have_many(:todos).dependent(:destroy) }
end
+ describe 'Included modules' do
+ it { is_expected.to include_module(Awardable) }
+ end
+
describe "Validation" do
before do
allow(subject).to receive(:set_iid).and_return(false)
@@ -199,12 +203,11 @@ describe Issue, "Issuable" do
end
end
+ # TODO ZJ
describe "votes" do
before do
- author = create :user
- project = create :empty_project
- issue.notes.awards.create!(note: "thumbsup", author: author, project: project)
- issue.notes.awards.create!(note: "thumbsdown", author: author, project: project)
+ create!(:award_emoji, :upvote, awardable: issue)
+ create!(:award_emoji, :downvote, awardable: issue)
end
it "returns correct values" do
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 4b788b57882..b479f1c2f1a 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -131,23 +131,6 @@ describe Note, models: true do
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 '#active?' do
it 'is always true when the note has no associated diff' do
note = build(:note)
@@ -218,11 +201,6 @@ describe Note, models: true do
note = build(:note, system: true)
expect(note.editable?).to be_falsy
end
-
- it "returns false" do
- note = build(:note, is_award: true, note: "smiley")
- expect(note.editable?).to be_falsy
- end
end
describe "cross_reference_not_visible_for?" do
@@ -249,23 +227,6 @@ describe Note, models: true do
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)
- expect(note.reload.note).to eq("thumbsup")
- end
-
- it "is not an award emoji when comment is on a diff" do
- note = create(:note, note: ":blowfish:", noteable: merge_request, 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
it 'clears a blank line code before validation' do
note = build(:note, line_code: ' ')
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 26d4e139396..b60725756cc 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -30,6 +30,7 @@ describe User, models: true do
it { is_expected.to have_one(:abuse_report) }
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(:award_emoji).dependent(:destroy) }
end
describe 'validations' do
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index ff23f13e1cb..93e8a7094e2 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -14,7 +14,7 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
-
+
@note = Notes::CreateService.new(project, user, opts).execute
end
@@ -28,18 +28,16 @@ describe Notes::CreateService, services: true do
project.team << [user, :master]
end
- it "creates emoji note" do
+ it "creates an award emoji" do
opts = {
note: ':smile: ',
noteable_type: 'Issue',
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('smile')
- expect(@note.is_award).to be_truthy
+ expect(note).to be_valid
+ expect(note.name).to eq('smile')
end
it "creates regular note if emoji name is invalid" do
@@ -48,12 +46,10 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
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])
- expect(@note.is_award).to be_falsy
+ expect(note).to be_valid
+ expect(note.note).to eq(opts[:note])
end
end
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index a075496ee63..0d88b77ad74 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -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_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(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') }
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
@@ -169,13 +168,6 @@ describe TodoService, services: true do
expect(second_todo.reload).to be_done
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
service.new_note(system_note, john_doe)
@@ -305,6 +297,15 @@ describe TodoService, services: true do
expect(second_todo.reload).to be_done
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
end
def should_create_todo(attributes = {})
diff --git a/spec/services/toggle_award_emoji_service_spec.rb b/spec/services/toggle_award_emoji_service_spec.rb
new file mode 100644
index 00000000000..3d2f497fdec
--- /dev/null
+++ b/spec/services/toggle_award_emoji_service_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe ToggleAwardEmoji, services: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe '#execute' do
+ it 'removes related todos' do
+ expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user)
+
+ ToggleAwardEmojiService.new(project, user).execute(issue, "thumbsdown")
+ end
+
+ it 'normalizes the emoji name' do
+ expect(issue).to receive(:toggle_award_emoji).with("thumbsup", user)
+
+ ToggleAwardEmojiService.new(project, user).execute(issue, ":+1:")
+ end
+
+ context 'when the emoji is set' do
+ it 'removes the emoji' do
+ create(:award_emoji, awardable: issue, user: user)
+
+ expect { ToggleAwardEmojiService.new(project, user).execute(issue, ":+1:") }.to change { AwardEmoji.count }.by(-1)
+ end
+ end
+
+ context 'when the award is not set yet' do
+ it 'awards the emoji' do
+ expect { ToggleAwardEmojiService.new(project, user).execute(issue, ":+1:") }.to change { AwardEmoji.count }.by(1)
+ end
+ end
+ end
+end