From 3b088fc5b53f03605484ebef1945b8839abe19de Mon Sep 17 00:00:00 2001
From: Zeger-Jan van de Weg <zegerjan@gitlab.com>
Date: Mon, 21 Mar 2016 14:12:52 +0100
Subject: [PATCH] Minor improvements on IssuableActions

---
 CHANGELOG                                     |  1 +
 app/controllers/concerns/issuable_action.rb   | 21 -------
 app/controllers/concerns/issuable_actions.rb  | 23 ++++++++
 app/controllers/projects/issues_controller.rb |  5 +-
 .../projects/merge_requests_controller.rb     |  5 +-
 app/models/ability.rb                         |  4 +-
 .../merge_requests/show/_mr_title.html.haml   |  2 +-
 app/views/shared/issuable/_form.html.haml     | 12 ++--
 db/schema.rb                                  |  1 +
 doc/api/issues.md                             | 34 ++---------
 doc/api/merge_requests.md                     | 56 +++----------------
 lib/api/issues.rb                             |  8 +--
 lib/api/merge_requests.rb                     |  6 +-
 .../projects/issues_controller_spec.rb        | 27 +++++----
 .../merge_requests_controller_spec.rb         | 24 ++++----
 spec/requests/api/issues_spec.rb              | 17 ++++--
 spec/requests/api/merge_requests_spec.rb      | 29 +++++-----
 17 files changed, 110 insertions(+), 165 deletions(-)
 delete mode 100644 app/controllers/concerns/issuable_action.rb
 create mode 100644 app/controllers/concerns/issuable_actions.rb

diff --git a/CHANGELOG b/CHANGELOG
index 3b3ea97c012..78418fd1b94 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -56,6 +56,7 @@ v 8.6.0 (unreleased)
   - User deletion is now done in the background so the request can not time out
   - Canceled builds are now ignored in compound build status if marked as `allowed to fail`
   - Trigger a todo for mentions on commits page
+  - Let project owners and admins soft delete issues and merge requests
 
 v 8.5.8
   - Bump Git version requirement to 2.7.4
diff --git a/app/controllers/concerns/issuable_action.rb b/app/controllers/concerns/issuable_action.rb
deleted file mode 100644
index d82f2bf9ef6..00000000000
--- a/app/controllers/concerns/issuable_action.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module IssuableAction
-  extend ActiveSupport::Concern
-
-  def destroy
-    issuable = @merge_request || @issue
-
-    unless current_user.can?(:"remove_#{issuable.to_ability_name}", issuable)
-      return access_denied!
-    end
-
-    issuable.destroy
-
-    route = polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
-    issuable_name = issuable.class.name.underscore.tr('_', ' ')
-
-    respond_to do |format|
-      format.html { redirect_to route, notice: "This #{issuable_name} was deleted." }
-      format.json { head :ok }
-    end
-  end
-end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
new file mode 100644
index 00000000000..f40b62446e5
--- /dev/null
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -0,0 +1,23 @@
+module IssuableActions
+  extend ActiveSupport::Concern
+
+  included do
+    before_action :authorize_destroy_issuable!, only: :destroy
+  end
+
+  def destroy
+    issuable.destroy
+
+    name = issuable.class.name.titleize.downcase
+    flash[:notice] = "The #{name} was successfully deleted."
+    redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
+  end
+
+  private
+
+  def authorize_destroy_issuable!
+    unless current_user.can?(:"destroy_#{issuable.to_ability_name}", issuable)
+      return access_denied!
+    end
+  end
+end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 75d48636da8..7c5a597cb6d 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -1,9 +1,9 @@
 class Projects::IssuesController < Projects::ApplicationController
   include ToggleSubscriptionAction
-  include IssuableAction
+  include IssuableActions
 
   before_action :module_enabled
-  before_action :issue, only: [:edit, :update, :show, :destroy]
+  before_action :issue, only: [:edit, :update, :show]
 
   # Allow read any issue
   before_action :authorize_read_issue!, only: [:show]
@@ -128,6 +128,7 @@ class Projects::IssuesController < Projects::ApplicationController
                end
   end
   alias_method :subscribable_resource, :issue
+  alias_method :issuable, :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 9cc0c8f64e3..e16acab696c 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -1,11 +1,11 @@
 class Projects::MergeRequestsController < Projects::ApplicationController
   include ToggleSubscriptionAction
   include DiffHelper
-  include IssuableAction
+  include IssuableActions
 
   before_action :module_enabled
   before_action :merge_request, only: [
-    :edit, :update, :show, :destroy, :diffs, :commits, :builds, :merge, :merge_check,
+    :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
     :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip
   ]
   before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
@@ -256,6 +256,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
   end
   alias_method :subscribable_resource, :merge_request
+  alias_method :issuable, :merge_request
 
   def closes_issues
     @closes_issues ||= @merge_request.closes_issues
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 8b3d4d4bd29..40c529e8117 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -236,8 +236,8 @@ class Ability
         :remove_project,
         :archive_project,
         :remove_fork_project,
-        :remove_merge_request,
-        :remove_issue
+        :destroy_merge_request,
+        :destroy_issue
       ]
     end
 
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index 806e4e83608..ab4b1f14be5 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -29,7 +29,7 @@
       - if @merge_request.open?
         = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: 'btn btn-nr btn-grouped btn-close', title: 'Close merge request'
         = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-nr btn-grouped issuable-edit', id: 'edit_merge_request' do
-          =icon('pencil-square-o')
+          = icon('pencil-square-o')
           Edit
       - if @merge_request.closed?
         = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'btn btn-nr btn-grouped btn-reopen reopen-mr-link', title: 'Reopen merge request'
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 4e1171cfb3d..3457c54bab9 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -114,13 +114,11 @@
       for this project.
 
   - if issuable.new_record?
-    = link_to namespace_project_issues_path(@project.namespace, @project), class: 'btn btn-cancel' do
-      Cancel
+    = link_to 'Cancel', namespace_project_issues_path(@project.namespace, @project), class: 'btn btn-cancel'
   - else
-    - if current_user.can?(:"remove_#{issuable.to_ability_name}", @project)
-      .pull-right
-        = link_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), method: :delete, class: 'btn' do
+    .pull-right
+      - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project)
+        = link_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), method: :delete, class: 'btn btn-grouped' do
           = icon('trash-o')
           Delete
-    = link_to namespace_project_issue_path(@project.namespace, @project, issuable), class: 'btn btn-cancel' do
-      Cancel
+      = link_to 'Cancel', namespace_project_issue_path(@project.namespace, @project, issuable), class: 'btn btn-grouped btn-cancel'
diff --git a/db/schema.rb b/db/schema.rb
index 1aa9d435153..a4a17770cd6 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -948,6 +948,7 @@ ActiveRecord::Schema.define(version: 20160316204731) do
     t.string   "unlock_token"
     t.datetime "otp_grace_period_started_at"
     t.boolean  "ldap_email",                  default: false, null: false
+    t.boolean  "external",                    default: false
   end
 
   add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
diff --git a/doc/api/issues.md b/doc/api/issues.md
index e2050db06d9..013ad9ffaa2 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -326,9 +326,11 @@ Example response:
 }
 ```
 
-## Delete existing issue
+## Delete an issue
 
-Only for admins and project owners. Soft deletes the issue in question. Returns the issue which was deleted.
+Only for admins and project owners. Soft deletes the issue in question.
+If the operation is successful, a status code of `200` is returned. Any the case you cannot
+destroy this issue, or it is not present, code `404` is given.
 
 ```
 DELETE /projects/:id/issues/:issue_id
@@ -343,34 +345,6 @@ DELETE /projects/:id/issues/:issue_id
 curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85
 ```
 
-Example response:
-
-```json
-{
-   "created_at" : "2016-01-07T12:46:01.410Z",
-   "author" : {
-      "name" : "Alexandra Bashirian",
-      "avatar_url" : null,
-      "username" : "eileen.lowe",
-      "id" : 18,
-      "state" : "active",
-      "web_url" : "https://gitlab.example.com/u/eileen.lowe"
-   },
-   "state" : "closed",
-   "title" : "Issues with auth",
-   "project_id" : 4,
-   "description" : null,
-   "updated_at" : "2016-01-07T12:55:16.213Z",
-   "iid" : 15,
-   "labels" : [
-      "bug"
-   ],
-   "id" : 85,
-   "assignee" : null,
-   "milestone" : null
-}
-```
-
 ## Comments on issues
 
 Comments are done via the [notes](notes.md) resource.
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 499e56c6395..88c3d251404 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -380,9 +380,12 @@ Parameters:
 If the operation is successful, 200 and the updated merge request is returned.
 If an error occurs, an error number and a message explaining the reason is returned.
 
-## Delete a MR
+## Delete a merge request
+
+Only for admins and project owners. Soft deletes the merge request in question.
+If the operation is successful, a status code of `200` is returned. Any the case you cannot
+destroy this merge request, or it is not present, code `404` is given.
 
-Soft deletes a merge request. For admins and owners only.
 
 ```
 DELETE /projects/:id/merge_requests/:merge_request_id
@@ -393,53 +396,8 @@ DELETE /projects/:id/merge_requests/:merge_request_id
 | `id`            | integer | yes | The ID of a project |
 | `merge_request_id` | integer | yes | The ID of a project's merge request |
 
-Example response:
-
-```json
-{
-  "id": 1,
-  "target_branch": "master",
-  "source_branch": "test1",
-  "project_id": 3,
-  "title": "test1",
-  "state": "merged",
-  "upvotes": 0,
-  "downvotes": 0,
-  "author": {
-    "id": 1,
-    "username": "admin",
-    "email": "admin@example.com",
-    "name": "Administrator",
-    "state": "active",
-    "created_at": "2012-04-29T08:46:00Z"
-  },
-  "assignee": {
-    "id": 1,
-    "username": "admin",
-    "email": "admin@example.com",
-    "name": "Administrator",
-    "state": "active",
-    "created_at": "2012-04-29T08:46:00Z"
-  },
-  "source_project_id": 4,
-  "target_project_id": 4,
-  "labels": [ ],
-  "description":"fixed login page css paddings",
-  "work_in_progress": false,
-  "milestone": {
-    "id": 5,
-    "iid": 1,
-    "project_id": 4,
-    "title": "v2.0",
-    "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.",
-    "state": "closed",
-    "created_at": "2015-02-02T19:49:26.013Z",
-    "updated_at": "2015-02-02T19:49:26.013Z",
-    "due_date": null
-  },
-  "merge_when_build_succeeds": true,
-  "merge_status": "can_be_merged"
-}
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85
 ```
 
 ## Accept MR
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 8c753e9f2ff..e5ae88eb96f 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -199,14 +199,10 @@ module API
       # Example Request:
       #   DELETE /projects/:id/issues/:issue_id
       delete ":id/issues/:issue_id" do
-        issue = user_project.issues.find(params[:issue_id])
-
-        authorize!(:remove_issue, issue)
+        issue = user_project.issues.find_by(id: params[:issue_id])
 
-        issue = user_project.issues.find(params[:issue_id])
+        authorize!(:destroy_issue, issue)
         issue.destroy
-
-        present issue, with: Entities::Issue
       end
     end
   end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index dc94cc5c85f..93052fba06b 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -106,12 +106,10 @@ module API
       # id (required)               - The ID of the project
       # merge_request_id (required) - The MR id
       delete ":id/merge_requests/:merge_request_id" do
-        merge_request = user_project.merge_requests.find(params[:merge_request_id])
+        merge_request = user_project.merge_requests.find_by(id: params[:merge_request_id])
 
-        authorize!(:remove_merge_request, merge_request)
+        authorize!(:destroy_merge_request, merge_request)
         merge_request.destroy
-
-        present merge_request, with: Entities::MergeRequest
       end
 
       # Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 62643a755b7..86930e317ce 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1,7 +1,7 @@
 require('spec_helper')
 
 describe Projects::IssuesController do
-  let(:project) { create(:project) }
+  let(:project)   { create(:project) }
   let(:user)    { create(:user) }
   let(:issue)   { create(:issue, project: project) }
 
@@ -188,21 +188,26 @@ describe Projects::IssuesController do
   end
 
   describe "DELETE #destroy" do
-    it "rejects a developer to destory an issue" do
-      delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
-      expect(response.status).to eq 404
+    context "when the user is a developer" do
+      before { sign_in(user) }
+      it "rejects a developer to destroy an issue" do
+        delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
+        expect(response.status).to eq(404)
+      end
     end
 
-    context "user is an admin" do
-      before do
-        user.admin = true
-        user.save
-      end
+    context "when the user is owner" do
+      let(:owner)     { create(:user) }
+      let(:namespace) { create(:namespace, owner: owner) }
+      let(:project)   { create(:project, namespace: namespace) }
+
+      before { sign_in owner }
 
-      it "lets an admin delete an issue" do
+      it "deletes the issue" do
         delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
 
-        expect(response.status).to eq 302
+        expect(response.status).to eq(302)
+        expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./).now
       end
     end
   end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index af154fadb6f..c5b034dc064 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -123,7 +123,7 @@ describe Projects::MergeRequestsController do
     end
   end
 
-  describe '#index' do
+  describe 'GET #index' do
     def get_merge_requests
       get :index,
           namespace_id: project.namespace.to_param,
@@ -157,23 +157,25 @@ describe Projects::MergeRequestsController do
     end
   end
 
-  describe "#destroy" do
-    it "lets mere mortals not access this endpoint" do
+  describe "DELETE #destroy" do
+    it "denies access to users unless they're admin or project owner" do
       delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
 
-      expect(response.status).to eq 404
+      expect(response.status).to eq(404)
     end
 
-    context "user is an admin or owner" do
-      before do
-        user.admin = true
-        user.save
-      end
+    context "when the user is owner" do
+      let(:owner)     { create(:user) }
+      let(:namespace) { create(:namespace, owner: owner) }
+      let(:project)   { create(:project, namespace: namespace) }
+
+      before { sign_in owner }
 
-      it "lets an admin or owner delete an issue" do
+      it "deletes the merge request" do
         delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
 
-        expect(response.status).to be 302
+        expect(response.status).to eq(302)
+        expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./).now
       end
     end
   end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 6298771925e..ce55cb7b0ae 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -469,20 +469,25 @@ describe API::API, api: true  do
   end
 
   describe "DELETE /projects/:id/issues/:issue_id" do
-    it "should reject a non member from deleting an issue" do
+    it "rejects a non member from deleting an issue" do
       delete api("/projects/#{project.id}/issues/#{issue.id}", non_member)
       expect(response.status).to be(403)
     end
 
-    it "should reject a developer from deleting an issue" do
+    it "rejects a developer from deleting an issue" do
       delete api("/projects/#{project.id}/issues/#{issue.id}", author)
       expect(response.status).to be(403)
     end
 
-    it "deletes the issue if an admin requests it" do
-      delete api("/projects/#{project.id}/issues/#{issue.id}", admin)
-      expect(response.status).to eq(200)
-      expect(json_response['state']).to eq 'opened'
+    context "when the user is project owner" do
+      let(:owner)     { create(:user) }
+      let(:project)   { create(:project, namespace: owner.namespace) }
+
+      it "deletes the issue if an admin requests it" do
+        delete api("/projects/#{project.id}/issues/#{issue.id}", owner)
+        expect(response.status).to eq(200)
+        expect(json_response['state']).to eq 'opened'
+      end
     end
   end
 end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 43e6a59d1ee..ae34684b932 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -317,23 +317,26 @@ describe API::API, api: true  do
     end
   end
 
-  describe "DELETE /projects/:id/merge_request/:merge_request_id" do
-    it "owners can destroy" do
-      delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+  describe "DELETE /projects/:id/merge_requests/:merge_request_id" do
+    context "when the user is developer" do
+      let(:developer) { create(:user) }
 
-      expect(response.status).to eq(200)
-    end
-
-    it "let's Admins and owners delete a merge request" do
-      delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", admin)
+      before do
+        project.team << [developer, :developer]
+      end
 
-      expect(response.status).to eq(200)
-      expect(json_response['id']).to eq merge_request.id
+      it "denies the deletion of the merge request" do
+        delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer)
+        expect(response.status).to be(403)
+      end
     end
 
-    it "rejects removal from other users" do
-      delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", non_member)
-      expect(response.status).to eq(404)
+    context "when the user is project owner" do
+      it "destroys the merge request owners can destroy" do
+        delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+
+        expect(response.status).to eq(200)
+      end
     end
   end
 
-- 
GitLab