Commit 0bcc3239 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'api/issues-filter-labels' into 'master'

API: Filter issues by labels

While working on [laboard](https://gitlab.com/jubianchi/laboard) I had some issues with the API : I was not able to filter issues by labels so I had to write some logic to do it on my side.

I think it will be useful to have this feature supported by the Gitlab API.

I added the filtering logic on `/issues` and `/projects/:id/issues`.

The commits are not squashed but I'll do it once everything seems ok to you.

See merge request !169
parents 18e4e997 0ac4a933
...@@ -12,6 +12,7 @@ v 7.3.0 ...@@ -12,6 +12,7 @@ v 7.3.0
- Deprecate LDAP account takeover based on partial LDAP email / GitLab username match - Deprecate LDAP account takeover based on partial LDAP email / GitLab username match
- Keyboard shortcuts for productivity (Robert Schilling) - Keyboard shortcuts for productivity (Robert Schilling)
- API: filter issues by state (Julien Bianchi) - API: filter issues by state (Julien Bianchi)
- API: filter issues by labels (Julien Bianchi)
- Add system hook for ssh key changes - Add system hook for ssh key changes
- Add blob permalink link (Ciro Santilli) - Add blob permalink link (Ciro Santilli)
......
...@@ -70,7 +70,7 @@ class Project < ActiveRecord::Base ...@@ -70,7 +70,7 @@ class Project < ActiveRecord::Base
has_many :merge_requests, dependent: :destroy, foreign_key: "target_project_id" has_many :merge_requests, dependent: :destroy, foreign_key: "target_project_id"
# Merge requests from source project should be kept when source project was removed # Merge requests from source project should be kept when source project was removed
has_many :fork_merge_requests, foreign_key: "source_project_id", class_name: MergeRequest has_many :fork_merge_requests, foreign_key: "source_project_id", class_name: MergeRequest
has_many :issues, -> { order "state DESC, created_at DESC" }, dependent: :destroy has_many :issues, -> { order 'issues.state DESC, issues.created_at DESC' }, dependent: :destroy
has_many :labels, dependent: :destroy has_many :labels, dependent: :destroy
has_many :services, dependent: :destroy has_many :services, dependent: :destroy
has_many :events, dependent: :destroy has_many :events, dependent: :destroy
......
...@@ -9,11 +9,15 @@ Get all issues created by authenticated user. This function takes pagination par ...@@ -9,11 +9,15 @@ Get all issues created by authenticated user. This function takes pagination par
GET /issues GET /issues
GET /issues?state=opened GET /issues?state=opened
GET /issues?state=closed GET /issues?state=closed
GET /issues?labels=foo
GET /issues?labels=foo,bar
GET /issues?labels=foo,bar&state=opened
``` ```
Parameters: Parameters:
- `state` (optional) - Return `all` issues or just those that are `opened` or `closed` - `state` (optional) - Return `all` issues or just those that are `opened` or `closed`
- `labels` (optional) - Comma-separated list of label names
```json ```json
[ [
...@@ -88,12 +92,16 @@ to return the list of project issues. ...@@ -88,12 +92,16 @@ to return the list of project issues.
GET /projects/:id/issues GET /projects/:id/issues
GET /projects/:id/issues?state=opened GET /projects/:id/issues?state=opened
GET /projects/:id/issues?state=closed 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
``` ```
Parameters: Parameters:
- `id` (required) - The ID of a project - `id` (required) - The ID of a project
- `state` (optional) - Return `all` issues or just those that are `opened` or `closed` - `state` (optional) - Return `all` issues or just those that are `opened` or `closed`
- `labels` (optional) - Comma-separated list of label names
## Single issue ## Single issue
......
...@@ -11,6 +11,10 @@ module API ...@@ -11,6 +11,10 @@ module API
else issues.order('id DESC') else issues.order('id DESC')
end end
end end
def filter_issues_labels(issues, labels)
issues.includes(:labels).where("labels.title" => labels.split(','))
end
end end
resource :issues do resource :issues do
...@@ -18,13 +22,21 @@ module API ...@@ -18,13 +22,21 @@ module API
# #
# Parameters: # Parameters:
# state (optional) - Return "opened" or "closed" issues # state (optional) - Return "opened" or "closed" issues
# # labels (optional) - Comma-separated list of label names
# Example Requests: # Example Requests:
# GET /issues # GET /issues
# GET /issues?state=opened # GET /issues?state=opened
# GET /issues?state=closed # GET /issues?state=closed
# GET /issues?labels=foo
# GET /issues?labels=foo,bar
# GET /issues?labels=foo,bar&state=opened
get do get do
present paginate(filter_issues_state(current_user.issues, params['state'])), with: Entities::Issue issues = current_user.issues
issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
present paginate(issues), with: Entities::Issue
end end
end end
...@@ -34,13 +46,22 @@ module API ...@@ -34,13 +46,22 @@ module API
# Parameters: # Parameters:
# id (required) - The ID of a project # id (required) - The ID of a project
# state (optional) - Return "opened" or "closed" issues # state (optional) - Return "opened" or "closed" issues
# labels (optional) - Comma-separated list of label names
# #
# Example Requests: # Example Requests:
# GET /projects/:id/issues # GET /projects/:id/issues
# GET /projects/:id/issues?state=opened # GET /projects/:id/issues?state=opened
# GET /projects/:id/issues?state=closed # 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 ":id/issues" do get ":id/issues" do
present paginate(filter_issues_state(user_project.issues, params['state'])), with: Entities::Issue 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?
present paginate(issues), with: Entities::Issue
end end
# Get a single project issue # Get a single project issue
......
...@@ -9,6 +9,7 @@ describe API::API, api: true do ...@@ -9,6 +9,7 @@ describe API::API, api: true do
let!(:label) do let!(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project) create(:label, title: 'label', color: '#FFAABB', project: project)
end end
let!(:label_link) { create(:label_link, label: label, target: issue) }
before { project.team << [user, :reporter] } before { project.team << [user, :reporter] }
...@@ -58,6 +59,45 @@ describe API::API, api: true do ...@@ -58,6 +59,45 @@ describe API::API, api: true do
json_response.first['id'].should == issue.id json_response.first['id'].should == issue.id
json_response.second['id'].should == closed_issue.id json_response.second['id'].should == closed_issue.id
end end
it 'should return an array of labeled issues' do
get api("/issues?labels=#{label.title}", user)
response.status.should == 200
json_response.should be_an Array
json_response.length.should == 1
json_response.first['labels'].should == [label.title]
end
it 'should return an array of labeled issues when at least one label matches' do
get api("/issues?labels=#{label.title},foo,bar", user)
response.status.should == 200
json_response.should be_an Array
json_response.length.should == 1
json_response.first['labels'].should == [label.title]
end
it 'should return an empty array if no issue matches labels' do
get api('/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 array of labeled issues matching given state' do
get api("/issues?labels=#{label.title}&state=opened", user)
response.status.should == 200
json_response.should be_an Array
json_response.length.should == 1
json_response.first['labels'].should == [label.title]
json_response.first['state'].should == 'opened'
end
it 'should return an empty array if no issue matches labels and state filters' do
get api("/issues?labels=#{label.title}&state=closed", user)
response.status.should == 200
json_response.should be_an Array
json_response.length.should == 0
end
end end
end end
...@@ -68,6 +108,29 @@ describe API::API, api: true do ...@@ -68,6 +108,29 @@ describe API::API, api: true do
json_response.should be_an Array json_response.should be_an Array
json_response.first['title'].should == issue.title json_response.first['title'].should == issue.title
end end
it 'should return an array of labeled project issues' do
get api("/projects/#{project.id}/issues?labels=#{label.title}", user)
response.status.should == 200
json_response.should be_an Array
json_response.length.should == 1
json_response.first['labels'].should == [label.title]
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)
response.status.should == 200
json_response.should be_an Array
json_response.length.should == 1
json_response.first['labels'].should == [label.title]
end
it 'should return an empty array if no project issue matches labels' do
get api("/projects/#{project.id}/issues?labels=foo,bar", user)
response.status.should == 200
json_response.should be_an Array
json_response.length.should == 0
end
end end
describe "GET /projects/:id/issues/:issue_id" do describe "GET /projects/:id/issues/:issue_id" do
...@@ -182,7 +245,7 @@ describe API::API, api: true do ...@@ -182,7 +245,7 @@ describe API::API, api: true do
labels: 'label2', state_event: "close" labels: 'label2', state_event: "close"
response.status.should == 200 response.status.should == 200
json_response['labels'].should == ['label2'] json_response['labels'].should include 'label2'
json_response['state'].should eq "closed" json_response['state'].should eq "closed"
end end
end end
......
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