Commit 3bdc57f0 authored by Zeger-Jan van de Weg's avatar Zeger-Jan van de Weg

Create table for award emoji

parent 05920a79
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
class Projects::IssuesController < Projects::ApplicationController
include ToggleSubscriptionAction
include IssuableActions
include ToggleAwardEmoji
before_action :module_enabled
before_action :issue,
......@@ -61,7 +62,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|
......@@ -158,6 +159,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)
......
......@@ -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
......
......@@ -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,35 +58,12 @@ 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
@note ||= @project.notes.find(params[:id])
end
alias_method :awardable, :note
def note_to_html(note)
render_to_string(
......@@ -137,7 +116,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 +124,7 @@ class Projects::NotesController < Projects::ApplicationController
else
{
valid: false,
award: note.is_award,
award: false,
errors: note.errors
}
end
......
......@@ -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
......
......@@ -144,16 +144,17 @@ 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
def award_user_list(awards, current_user)
list =
awards.map do |award|
award.user == current_user ? "me" : award.user.name
end
list.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
""
......
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
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
......@@ -10,6 +10,7 @@ module Issuable
include Mentionable
include Subscribable
include StripAttribute
include Awardable
included do
belongs_to :author, class_name: "User"
......@@ -99,29 +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
end
def today?
......@@ -144,14 +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 subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
......
......@@ -36,6 +36,7 @@ class MergeRequest < ActiveRecord::Base
include Referable
include Sortable
include Taskable
include Awardable
belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
......
......@@ -16,7 +16,6 @@
# system :boolean default(FALSE), not null
# st_diff :text
# updated_by_id :integer
# is_award :boolean default(FALSE), not null
#
require 'carrierwave/orm/activerecord'
......@@ -43,12 +42,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 }
......@@ -60,8 +56,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) }
......@@ -119,19 +113,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?
......@@ -347,37 +328,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?
......@@ -389,8 +358,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?
......@@ -399,6 +368,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
......@@ -144,6 +144,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
......
......@@ -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
if note.save
# Finish the harder work in the background
NewNoteWorker.perform_in(2.seconds, note.id, params)
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
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
- grouped_awards = 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_awards).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), title: award_user_list(awards, current_user), data: { placement: "bottom" } }
= 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
.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
......
......@@ -27,7 +27,7 @@
= icon('thumbs-down')
= downvotes
- note_count = issue.notes.user.nonawards.count
- note_count = issue.notes.user.count
- if note_count > 0
%li
= link_to issue_path(issue) + "#notes" do
......
......@@ -72,7 +72,7 @@
.content-block.content-block-small
= render 'new_branch'
= render 'votes/votes_block', votable: @issue
= render 'award_emoji/awards_block', awardable: @issue, inline: true
.row
%section.col-md-12
......
......@@ -35,7 +35,7 @@
= icon('thumbs-down')
= downvotes
- note_count = merge_request.mr_and_commit_notes.user.nonawards.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
......
......@@ -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
......
.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
......
......@@ -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
......@@ -639,6 +639,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
......@@ -703,6 +704,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
end
......@@ -731,10 +733,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
......
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
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')
)