diff --git a/CHANGELOG b/CHANGELOG index c1531cb2ff2757995fc4bb967fa5946ee75ef06b..0f2b97e7c2b516093732e6f38b7dd3b4e6559eb3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 7.3.0 - Deprecate LDAP account takeover based on partial LDAP email / GitLab username match - Keyboard shortcuts for productivity (Robert Schilling) - API: filter issues by state (Julien Bianchi) + - API: filter issues by labels (Julien Bianchi) - Add system hook for ssh key changes v 7.2.0 diff --git a/app/models/project.rb b/app/models/project.rb index 5cc35f20cadf4de65251e3453f22b60f712ad192..2bd6a57ee287c8d2c0881b450e805663b0d762e3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -70,7 +70,7 @@ class Project < ActiveRecord::Base has_many :merge_requests, dependent: :destroy, foreign_key: "target_project_id" # 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 :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 :services, dependent: :destroy has_many :events, dependent: :destroy diff --git a/doc/api/issues.md b/doc/api/issues.md index c12d45285469419186a983c27c7110bfb8faa620..a935b146d37e5b5d08bbda94a021730be4c81230 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -9,11 +9,15 @@ Get all issues created by authenticated user. This function takes pagination par GET /issues GET /issues?state=opened GET /issues?state=closed +GET /issues?labels=foo +GET /issues?labels=foo,bar +GET /issues?labels=foo,bar&state=opened ``` Parameters: - `state` (optional) - Return `all` issues or just those that are `opened` or `closed` +- `labels` (optional) - Comma-separated list of label names ```json [ @@ -88,12 +92,16 @@ to return the list of project issues. GET /projects/:id/issues GET /projects/:id/issues?state=opened 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: - `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 ## Single issue diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 15a49b452bd7689ac3a7ef4f074e3e94fab19e71..e4a66eceadde88b997e1011c402c653a2966f570 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -11,6 +11,10 @@ module API else issues.order('id DESC') end end + + def filter_issues_labels(issues, labels) + issues.includes(:labels).where("labels.title" => labels.split(',')) + end end resource :issues do @@ -18,13 +22,21 @@ module API # # Parameters: # state (optional) - Return "opened" or "closed" issues - # + # labels (optional) - Comma-separated list of label names + # Example Requests: # GET /issues # GET /issues?state=opened # GET /issues?state=closed + # GET /issues?labels=foo + # GET /issues?labels=foo,bar + # GET /issues?labels=foo,bar&state=opened 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 @@ -34,13 +46,22 @@ module API # Parameters: # id (required) - The ID of a project # state (optional) - Return "opened" or "closed" issues + # labels (optional) - Comma-separated list of label names # # 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 ":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 # Get a single project issue diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index f70b56b194f13fec954c301bedb4afbffbf70dc3..e8eebda95b40943b24fcd94e6821081561b128f3 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -9,6 +9,7 @@ describe API::API, api: true do let!(:label) do create(:label, title: 'label', color: '#FFAABB', project: project) end + let!(:label_link) { create(:label_link, label: label, target: issue) } before { project.team << [user, :reporter] } @@ -58,6 +59,45 @@ describe API::API, api: true do json_response.first['id'].should == issue.id json_response.second['id'].should == closed_issue.id 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 @@ -68,6 +108,29 @@ describe API::API, api: true do 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) + 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 describe "GET /projects/:id/issues/:issue_id" do @@ -182,7 +245,7 @@ describe API::API, api: true do labels: 'label2', state_event: "close" response.status.should == 200 - json_response['labels'].should == ['label2'] + json_response['labels'].should include 'label2' json_response['state'].should eq "closed" end end