Commit 24fed567 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'and-you-get-awards' into 'master'

And Snippets get awards

## What does this MR do?

Makes snippets more awesome, by making them awardables

## Why was this MR needed?

Because Snippets were left behind.

## What are the relevant issue numbers?

Closes #17878

See merge request !4456
parents ab49c1a3 e41a3912
......@@ -52,6 +52,7 @@ v 8.12.0 (unreleased)
- Move parsing of sidekiq ps into helper !6245 (pascalbetz)
- Added go to issue boards keyboard shortcut
- Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel)
- Emoji can be awarded on Snippets !4456
- Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
- Fix blame table layout width
- Spec testing if issue authors can read issues on private projects
......
......@@ -12,11 +12,18 @@
.snippet-file-content {
border-radius: 3px;
margin-bottom: $gl-padding;
.btn-clipboard {
@extend .btn;
}
}
.project-snippets .awards {
border-bottom: 1px solid $table-border-color;
padding-bottom: $gl-padding;
}
.snippet-title {
font-size: 24px;
font-weight: 600;
......
......@@ -10,7 +10,9 @@ module ToggleAwardEmoji
if awardable.user_can_award?(current_user, name)
awardable.toggle_award_emoji(name, current_user)
TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
todoable = to_todoable(awardable)
TodoService.new.new_award_emoji(todoable, current_user) if todoable
render json: { ok: true }
else
......@@ -24,8 +26,10 @@ module ToggleAwardEmoji
case awardable
when Note
awardable.noteable
else
when MergeRequest, Issue
awardable
when Snippet
nil
end
end
......
class Projects::SnippetsController < Projects::ApplicationController
include ToggleAwardEmoji
before_action :module_enabled
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji]
# Allow read any snippet
before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
......@@ -80,6 +82,7 @@ class Projects::SnippetsController < Projects::ApplicationController
def snippet
@snippet ||= @project.snippets.find(params[:id])
end
alias_method :awardable, :snippet
def authorize_read_project_snippet!
return render_404 unless can?(current_user, :read_project_snippet, @snippet)
......
class SnippetsController < ApplicationController
include ToggleAwardEmoji
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet
......@@ -85,6 +87,7 @@ class SnippetsController < ApplicationController
PersonalSnippet.find(params[:id])
end
end
alias_method :awardable, :snippet
def authorize_read_snippet!
authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
......
module AwardEmojiHelper
def toggle_award_url(awardable)
if @project
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
else
url_for([:toggle_award_emoji, awardable])
end
end
end
......@@ -106,6 +106,14 @@ module GitlabRoutingHelper
end
end
def toggle_award_emoji_personal_snippet_path(*args)
toggle_award_emoji_snippet_path(*args)
end
def toggle_award_emoji_namespace_project_project_snippet_path(*args)
toggle_award_emoji_namespace_project_snippet_path(*args)
end
## Members
def project_members_url(project, *args)
namespace_project_project_members_url(project.namespace, project)
......
......@@ -71,6 +71,12 @@ module Awardable
end
end
def user_authored?(current_user)
author = self.respond_to?(:author) ? self.author : self.user
author == current_user
end
def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: current_user).exists?
end
......
......@@ -200,10 +200,6 @@ module Issuable
end
end
def user_authored?(user)
user == author
end
def subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
......
......@@ -223,10 +223,6 @@ class Note < ActiveRecord::Base
end
end
def user_authored?(user)
user == author
end
def award_emoji?
can_be_award_emoji? && contains_emoji_only?
end
......
......@@ -4,6 +4,7 @@ class Snippet < ActiveRecord::Base
include Participable
include Referable
include Sortable
include Awardable
default_value_for :visibility_level, Snippet::PRIVATE
......
- 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.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(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, sprite: false)
......
......@@ -3,7 +3,7 @@
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do
New Snippet
- if can?(current_user, :update_project_snippet, @snippet)
= link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do
= link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
Delete
- if can?(current_user, :update_project_snippet, @snippet)
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do
......
......@@ -2,13 +2,16 @@
= render 'shared/snippets/header'
%article.file-holder.snippet-file-content
.file-title
= blob_icon 0, @snippet.file_name
= @snippet.file_name
.file-actions
= clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
= link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
= render 'shared/snippets/blob'
%div#notes= render "projects/notes/notes_with_form"
.project-snippets
%article.file-holder.snippet-file-content
.file-title
= blob_icon 0, @snippet.file_name
= @snippet.file_name
.file-actions
= clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
= link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
= render 'shared/snippets/blob'
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
%div#notes= render "projects/notes/notes_with_form"
......@@ -10,3 +10,5 @@
= clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
= link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
= render 'shared/snippets/blob'
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
\ No newline at end of file
......@@ -35,6 +35,10 @@ Rails.application.routes.draw do
post :approve_access_request, on: :member
end
concern :awardable do
post :toggle_award_emoji, on: :member
end
namespace :ci do
# CI API
Ci::API::API.logger Rails.logger
......@@ -98,7 +102,7 @@ Rails.application.routes.draw do
#
# Global snippets
#
resources :snippets do
resources :snippets, concerns: :awardable do
member do
get 'raw'
end
......@@ -110,7 +114,6 @@ Rails.application.routes.draw do
#
# Invites
#
resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do
member do
post :accept
......@@ -662,7 +665,7 @@ Rails.application.routes.draw do
end
end
resources :snippets, constraints: { id: /\d+/ } do
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get 'raw'
end
......@@ -724,7 +727,7 @@ Rails.application.routes.draw do
end
end
resources :merge_requests, constraints: { id: /\d+/ } do
resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get :commits
get :diffs
......@@ -736,7 +739,6 @@ Rails.application.routes.draw do
post :cancel_merge_when_build_succeeds
get :ci_status
post :toggle_subscription
post :toggle_award_emoji
post :remove_wip
get :diff_for_path
post :resolve_conflicts
......@@ -840,10 +842,9 @@ Rails.application.routes.draw do
end
end
resources :issues, constraints: { id: /\d+/ } do
resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
post :toggle_award_emoji
post :mark_as_spam
get :referenced_merge_requests
get :related_branches
......@@ -871,9 +872,8 @@ Rails.application.routes.draw do
resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
member do
post :toggle_award_emoji
delete :delete_attachment
post :resolve
delete :resolve, action: :unresolve
......
# Award Emoji
> [Introduced][ce-4575] in GitLab 8.9.
> [Introduced][ce-4575] in GitLab 8.9, Snippet support in 8.12
An awarded emoji tells a thousand words, and can be awarded on issues, merge
requests and notes/comments. Issues, merge requests and notes are further called
requests, snippets, and notes/comments. Issues, merge requests, snippets, and notes are further called
`awardables`.
## Issues and merge requests
## Issues, merge requests, and snippets
### List an awardable's award emoji
......@@ -15,6 +16,7 @@ Gets a list of all award emoji
```
GET /projects/:id/issues/:issue_id/award_emoji
GET /projects/:id/merge_requests/:merge_request_id/award_emoji
GET /projects/:id/snippets/:snippet_id/award_emoji
```
Parameters:
......@@ -69,11 +71,12 @@ Example Response:
### Get single award emoji
Gets a single award emoji from an issue or merge request.
Gets a single award emoji from an issue, snippet, or merge request.
```
GET /projects/:id/issues/:issue_id/award_emoji/:award_id
GET /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id
```
Parameters:
......@@ -116,6 +119,7 @@ This end point creates an award emoji on the specified resource
```
POST /projects/:id/issues/:issue_id/award_emoji
POST /projects/:id/merge_requests/:merge_request_id/award_emoji
POST /projects/:id/snippets/:snippet_id/award_emoji
```
Parameters:
......@@ -159,6 +163,7 @@ admins or the author of the award. Status code 200 on success, 401 if unauthoriz
```
DELETE /projects/:id/issues/:issue_id/award_emoji/:award_id
DELETE /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id
```
Parameters:
......@@ -197,7 +202,7 @@ Example Response:
## Award Emoji on Notes
The endpoints documented above are available for Notes as well. Notes
are a sub-resource of Issues and Merge Requests. The examples below
are a sub-resource of Issues, Merge Requests, or Snippets. The examples below
describe working with Award Emoji on notes for an Issue, but can be
easily adapted for notes on a Merge Request.
......
module API
class AwardEmoji < Grape::API
before { authenticate! }
AWARDABLES = [Issue, MergeRequest]
AWARDABLES = %w[issue merge_request snippet]
resource :projects do
AWARDABLES.each do |awardable_type|
awardable_string = awardable_type.to_s.underscore.pluralize
awardable_id_string = "#{awardable_type.to_s.underscore}_id"
awardable_string = awardable_type.pluralize
awardable_id_string = "#{awardable_type}_id"
[ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
......@@ -87,9 +87,7 @@ module API
helpers do
def can_read_awardable?
ability = "read_#{awardable.class.to_s.underscore}".to_sym
can?(current_user, ability, awardable)
can?(current_user, read_ability(awardable), awardable)
end
def can_award_awardable?
......@@ -100,18 +98,25 @@ module API
@awardable ||=
begin
if params.include?(:note_id)
noteable.notes.find(params[:note_id])
note_id = params.delete(:note_id)
awardable.notes.find(note_id)
elsif params.include?(:issue_id)
user_project.issues.find(params[:issue_id])
elsif params.include?(:merge_request_id)
user_project.merge_requests.find(params[:merge_request_id])
else
noteable
user_project.snippets.find(params[:snippet_id])
end
end
end
def noteable
if params.include?(:issue_id)
user_project.issues.find(params[:issue_id])
def read_ability(awardable)
case awardable
when Note
read_ability(awardable.noteable)
else
user_project.merge_requests.find(params[:merge_request_id])
:"read_#{awardable.class.to_s.underscore}"
end
end
end
......
require 'spec_helper'
describe SnippetsController do
describe 'GET #show' do
let(:user) { create(:user) }
let(:user) { create(:user) }
describe 'GET #show' do
context 'when the personal snippet is private' do
let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
......@@ -230,4 +230,33 @@ describe SnippetsController do
end
end
end
context 'award emoji on snippets' do
let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
let(:another_user) { create(:user) }
before do
sign_in(another_user)
end
describe 'POST #toggle_award_emoji' do
it "toggles the award emoji" do
expect do
post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup")
end.to change { personal_snippet.award_emoji.count }.from(0).to(1)
expect(response.status).to eq(200)
end
it "removes the already awarded emoji" do
post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup")
expect do
post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup")
end.to change { personal_snippet.award_emoji.count }.from(1).to(0)
expect(response.status).to eq(200)
end
end
end
end
......@@ -9,12 +9,14 @@ describe Snippet, models: true do
it { is_expected.to include_module(Participable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Awardable) }
end
describe 'associations' do
it { is_expected.to belong_to(:author).class_name('User') }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
end
describe 'validation' do
......
......@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let!(:project) { create(:project) }
let!(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
......@@ -39,6 +39,19 @@ describe API::API, api: true do
end
end
context 'on a snippet' do
let(:snippet) { create(:project_snippet, :public, project: project) }
let!(:award) { create(:award_emoji, awardable: snippet) }
it 'returns the awarded emoji' do
get api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(award.name)
end
end
context 'when the user has no access' do
it 'returns a status code 404' do
user1 = create(:user)
......@@ -91,6 +104,20 @@ describe API::API, api: true do
end
end
context 'on a snippet' do
let(:snippet) { create(:project_snippet, :public, project: project) }
let!(:award) { create(:award_emoji, awardable: snippet) }
it 'returns the awarded emoji' do
get api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(award.name)
expect(json_response['awardable_id']).to eq(snippet.id)
expect(json_response['awardable_type']).to eq("Snippet")
end
end
context 'when the user has no access' do
it 'returns a status code 404' do
user1 = create(:user)
......@@ -160,6 +187,18 @@ describe API::API, api: true do
end
end
end
context 'on a snippet' do
it 'creates a new award emoji' do
snippet = create(:project_snippet, :public, project: project)
post api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish'
expect(response).to have_http_status(201)
expect(json_response['name']).to eq('blowfish')
expect(json_response['user']['username']).to eq(user.username)
end
end
end
describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do
......@@ -229,6 +268,19 @@ describe API::API, api: true do
expect(response).to have_http_status(404)
end
end
context 'when the awardable is a Snippet' do
let(:snippet) { create(:project_snippet, :public, project: project) }
let!(:award) { create(:award_emoji, awardable: snippet, user: user) }
it 'deletes the award' do
expect do
delete api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
end.to change { snippet.award_emoji.count }.from(1).to(0)
expect(response).to have_http_status(200)
end
end
end
describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do
......
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