From f258a59ef6156f9da3b527efe9088fd0708f6fdf Mon Sep 17 00:00:00 2001
From: jubianchi <contact@jubianchi.fr>
Date: Fri, 5 Sep 2014 00:01:12 +0200
Subject: [PATCH] Filters issues by milestone via API

---
 CHANGELOG                        |  1 +
 doc/api/issues.md                |  3 ++
 lib/api/issues.rb                | 15 ++++++--
 spec/requests/api/issues_spec.rb | 64 +++++++++++++++++++++++++++++---
 4 files changed, 74 insertions(+), 9 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 3006ff4049d..7e638b293bc 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -5,6 +5,7 @@ v 7.4.0
   - Refactor test coverage tools usage. Use SIMPLECOV=true to generate it locally
   - Increase unicorn timeout to 60 seconds
   - Sort search autocomplete projects by stars count so most popular go first
+  - API: filter project issues by milestone (Julien Bianchi)
 
 v 7.3.1
   - Fix ref parsing in Gitlab::GitAccess
diff --git a/doc/api/issues.md b/doc/api/issues.md
index a935b146d37..ceeb683a6bf 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -95,6 +95,8 @@ GET /projects/:id/issues?state=closed
 GET /projects/:id/issues?labels=foo
 GET /projects/:id/issues?labels=foo,bar
 GET /projects/:id/issues?labels=foo,bar&state=opened
+GET /projects/:id/issues?milestone=1.0.0
+GET /projects/:id/issues?milestone=1.0.0&state=opened
 ```
 
 Parameters:
@@ -102,6 +104,7 @@ Parameters:
 - `id` (required) - The ID of a project
 - `state` (optional) - Return `all` issues or just those that are `opened` or `closed`
 - `labels` (optional) - Comma-separated list of label names
+- `milestone` (optional) - Milestone title
 
 ## Single issue
 
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 30170c657ba..d2828b24c36 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -4,7 +4,7 @@ module API
     before { authenticate! }
 
     helpers do
-      def filter_issues_state(issues, state = nil)
+      def filter_issues_state(issues, state)
         case state
         when 'opened' then issues.opened
         when 'closed' then issues.closed
@@ -13,7 +13,11 @@ module API
       end
 
       def filter_issues_labels(issues, labels)
-        issues.includes(:labels).where("labels.title" => labels.split(','))
+        issues.includes(:labels).where('labels.title' => labels.split(','))
+      end
+
+      def filter_issues_milestone(issues, milestone)
+        issues.includes(:milestone).where('milestones.title' => milestone)
       end
     end
 
@@ -48,19 +52,24 @@ module API
       #   id (required) - The ID of a project
       #   state (optional) - Return "opened" or "closed" issues
       #   labels (optional) - Comma-separated list of label names
+      #   milestone (optional) - Milestone title
       #
       # Example Requests:
       #   GET /projects/:id/issues
       #   GET /projects/:id/issues?state=opened
       #   GET /projects/:id/issues?state=closed
-      #   GET /projects/:id/issues
       #   GET /projects/:id/issues?labels=foo
       #   GET /projects/:id/issues?labels=foo,bar
       #   GET /projects/:id/issues?labels=foo,bar&state=opened
+      #   GET /projects/:id/issues?milestone=1.0.0
+      #   GET /projects/:id/issues?milestone=1.0.0&state=closed
       get ":id/issues" do
         issues = user_project.issues
         issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
         issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
+        unless params[:milestone].nil?
+          issues = filter_issues_milestone(issues, params[:milestone])
+        end
         issues = issues.order('issues.id DESC')
 
         present paginate(issues), with: Entities::Issue
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 9876452f81d..775d7b4e18d 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -4,12 +4,29 @@ describe API::API, api: true  do
   include ApiHelpers
   let(:user) { create(:user) }
   let!(:project) { create(:project, namespace: user.namespace ) }
-  let!(:closed_issue) { create(:closed_issue, author: user, assignee: user, project: project, state: :closed) }
-  let!(:issue) { create(:issue, author: user, assignee: user, project: project) }
+  let!(:closed_issue) do
+    create :closed_issue,
+           author: user,
+           assignee: user,
+           project: project,
+           state: :closed,
+           milestone: milestone
+  end
+  let!(:issue) do
+    create :issue,
+           author: user,
+           assignee: user,
+           project: project,
+           milestone: milestone
+  end
   let!(:label) do
     create(:label, title: 'label', color: '#FFAABB', project: project)
   end
   let!(:label_link) { create(:label_link, label: label, target: issue) }
+  let!(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+  let!(:empty_milestone) do
+    create(:milestone, title: '2.0.0', project: project)
+  end
 
   before { project.team << [user, :reporter] }
 
@@ -102,15 +119,18 @@ describe API::API, api: true  do
   end
 
   describe "GET /projects/:id/issues" do
+    let(:base_url) { "/projects/#{project.id}" }
+    let(:title) { milestone.title }
+
     it "should return project issues" do
-      get api("/projects/#{project.id}/issues", user)
+      get api("#{base_url}/issues", user)
       response.status.should == 200
       json_response.should be_an Array
       json_response.first['title'].should == issue.title
     end
 
     it 'should return an array of labeled project issues' do
-      get api("/projects/#{project.id}/issues?labels=#{label.title}", user)
+      get api("#{base_url}/issues?labels=#{label.title}", user)
       response.status.should == 200
       json_response.should be_an Array
       json_response.length.should == 1
@@ -118,7 +138,7 @@ describe API::API, api: true  do
     end
 
     it 'should return an array of labeled project issues when at least one label matches' do
-      get api("/projects/#{project.id}/issues?labels=#{label.title},foo,bar", user)
+      get api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
       response.status.should == 200
       json_response.should be_an Array
       json_response.length.should == 1
@@ -126,11 +146,43 @@ describe API::API, api: true  do
     end
 
     it 'should return an empty array if no project issue matches labels' do
-      get api("/projects/#{project.id}/issues?labels=foo,bar", user)
+      get api("#{base_url}/issues?labels=foo,bar", user)
+      response.status.should == 200
+      json_response.should be_an Array
+      json_response.length.should == 0
+    end
+
+    it 'should return an empty array if no issue matches milestone' do
+      get api("#{base_url}/issues?milestone=#{empty_milestone.title}", user)
       response.status.should == 200
       json_response.should be_an Array
       json_response.length.should == 0
     end
+
+    it 'should return an empty array if milestone does not exist' do
+      get api("#{base_url}/issues?milestone=foo", user)
+      response.status.should == 200
+      json_response.should be_an Array
+      json_response.length.should == 0
+    end
+
+    it 'should return an array of issues in given milestone' do
+      get api("#{base_url}/issues?milestone=#{title}", user)
+      response.status.should == 200
+      json_response.should be_an Array
+      json_response.length.should == 2
+      json_response.first['id'].should == issue.id
+      json_response.second['id'].should == closed_issue.id
+    end
+
+    it 'should return an array of issues matching state in milestone' do
+      get api("#{base_url}/issues?milestone=#{milestone.title}"\
+              '&state=closed', user)
+      response.status.should == 200
+      json_response.should be_an Array
+      json_response.length.should == 1
+      json_response.first['id'].should == closed_issue.id
+    end
   end
 
   describe "GET /projects/:id/issues/:issue_id" do
-- 
GitLab