issues.rb 13.1 KB
Newer Older
1
module API
Nihad Abbasov's avatar
Nihad Abbasov committed
2
  class Issues < Grape::API
Robert Schilling's avatar
Robert Schilling committed
3 4
    include PaginationParams

5
    before { authenticate_non_get! }
Nihad Abbasov's avatar
Nihad Abbasov committed
6

7 8
    helpers ::Gitlab::IssuableMetadata

jubianchi's avatar
jubianchi committed
9
    helpers do
10
      def find_issues(args = {})
11
        args = declared_params.merge(args)
12 13 14

        args.delete(:id)
        args[:milestone_title] = args.delete(:milestone)
15
        args[:label_name] = args.delete(:labels)
16
        args[:scope] = args[:scope].underscore if args[:scope]
17

18
        issues = IssuesFinder.new(current_user, args).execute
19
          .preload(:assignees, :labels, :notes, :timelogs)
20 21

        issues.reorder(args[:order_by] => args[:sort])
22 23
      end

Robert Schilling's avatar
Robert Schilling committed
24 25
      params :issues_params do
        optional :labels, type: String, desc: 'Comma-separated list of label names'
26
        optional :milestone, type: String, desc: 'Milestone title'
Robert Schilling's avatar
Robert Schilling committed
27 28 29 30
        optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
                            desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
        optional :sort, type: String, values: %w[asc desc], default: 'desc',
                        desc: 'Return issues sorted in `asc` or `desc` order.'
31
        optional :milestone, type: String, desc: 'Return issues for a specific milestone'
32
        optional :iids, type: Array[Integer], desc: 'The IID array of issues'
33
        optional :search, type: String, desc: 'Search issues for text present in the title or description'
34 35
        optional :created_after, type: DateTime, desc: 'Return issues created after the specified time'
        optional :created_before, type: DateTime, desc: 'Return issues created before the specified time'
36 37
        optional :updated_after, type: DateTime, desc: 'Return issues updated after the specified time'
        optional :updated_before, type: DateTime, desc: 'Return issues updated before the specified time'
38 39
        optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID'
        optional :assignee_id, type: Integer, desc: 'Return issues which are assigned to the user with the given ID'
40 41
        optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all],
                         desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
42
        optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
Robert Schilling's avatar
Robert Schilling committed
43 44
        use :pagination
      end
45

46
      params :issue_params_ce do
Robert Schilling's avatar
Robert Schilling committed
47
        optional :description, type: String, desc: 'The description of an issue'
48 49
        optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
        optional :assignee_id,  type: Integer, desc: '[Deprecated] The ID of a user to assign issue'
Robert Schilling's avatar
Robert Schilling committed
50 51
        optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue'
        optional :labels, type: String, desc: 'Comma-separated list of label names'
52
        optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY'
Robert Schilling's avatar
Robert Schilling committed
53
        optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
54
        optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked"
55 56 57
      end

      params :issue_params_ee do
58
        optional :weight, type: Integer, values: 0..9, desc: 'The weight of the issue'
59
      end
60 61 62 63 64

      params :issue_params do
        use :issue_params_ce
        use :issue_params_ee
      end
jubianchi's avatar
jubianchi committed
65 66
    end

Nihad Abbasov's avatar
Nihad Abbasov committed
67
    resource :issues do
Robert Schilling's avatar
Robert Schilling committed
68
      desc "Get currently authenticated user's issues" do
69
        success Entities::IssueBasic
Robert Schilling's avatar
Robert Schilling committed
70 71 72 73 74
      end
      params do
        optional :state, type: String, values: %w[opened closed all], default: 'all',
                         desc: 'Return opened, closed, or all issues'
        use :issues_params
75 76
        optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], default: 'created_by_me',
                         desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
Robert Schilling's avatar
Robert Schilling committed
77
      end
Nihad Abbasov's avatar
Nihad Abbasov committed
78
      get do
79
        authenticate! unless params[:scope] == 'all'
80
        issues = paginate(find_issues)
Sean McGivern's avatar
Sean McGivern committed
81

82 83 84 85 86
        options = {
          with: Entities::IssueBasic,
          current_user: current_user,
          issuable_metadata: issuable_meta_data(issues, 'Issue')
        }
87

88
        present issues, options
Nihad Abbasov's avatar
Nihad Abbasov committed
89 90 91
      end
    end

Robert Schilling's avatar
Robert Schilling committed
92 93 94
    params do
      requires :id, type: String, desc: 'The ID of a group'
    end
95
    resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
Robert Schilling's avatar
Robert Schilling committed
96
      desc 'Get a list of group issues' do
97
        success Entities::IssueBasic
Robert Schilling's avatar
Robert Schilling committed
98 99
      end
      params do
100
        optional :state, type: String, values: %w[opened closed all], default: 'all',
Robert Schilling's avatar
Robert Schilling committed
101 102 103
                         desc: 'Return opened, closed, or all issues'
        use :issues_params
      end
104
      get ":id/issues" do
105
        group = find_group!(params[:id])
Sean McGivern's avatar
Sean McGivern committed
106

107
        issues = paginate(find_issues(group_id: group.id, include_subgroups: true))
108

109 110 111 112 113
        options = {
          with: Entities::IssueBasic,
          current_user: current_user,
          issuable_metadata: issuable_meta_data(issues, 'Issue')
        }
114

115
        present issues, options
116 117 118
      end
    end

119 120 121
    params do
      requires :id, type: String, desc: 'The ID of a project'
    end
122
    resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
123 124
      include TimeTrackingEndpoints

Robert Schilling's avatar
Robert Schilling committed
125
      desc 'Get a list of project issues' do
126
        success Entities::IssueBasic
Robert Schilling's avatar
Robert Schilling committed
127 128 129 130 131 132
      end
      params do
        optional :state, type: String, values: %w[opened closed all], default: 'all',
                         desc: 'Return opened, closed, or all issues'
        use :issues_params
      end
Nihad Abbasov's avatar
Nihad Abbasov committed
133
      get ":id/issues" do
134
        project = find_project!(params[:id])
135

136
        issues = paginate(find_issues(project_id: project.id))
137

138 139 140 141 142 143
        options = {
          with: Entities::IssueBasic,
          current_user: current_user,
          project: user_project,
          issuable_metadata: issuable_meta_data(issues, 'Issue')
        }
144

145
        present issues, options
Nihad Abbasov's avatar
Nihad Abbasov committed
146 147
      end

Robert Schilling's avatar
Robert Schilling committed
148 149 150 151
      desc 'Get a single project issue' do
        success Entities::Issue
      end
      params do
152
        requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
Robert Schilling's avatar
Robert Schilling committed
153
      end
154
      get ":id/issues/:issue_iid", as: :api_v4_project_issue do
155
        issue = find_project_issue(params[:issue_iid])
Robert Schilling's avatar
Robert Schilling committed
156
        present issue, with: Entities::Issue, current_user: current_user, project: user_project
Nihad Abbasov's avatar
Nihad Abbasov committed
157 158
      end

Robert Schilling's avatar
Robert Schilling committed
159 160 161 162 163 164 165
      desc 'Create a new project issue' do
        success Entities::Issue
      end
      params do
        requires :title, type: String, desc: 'The title of an issue'
        optional :created_at, type: DateTime,
                              desc: 'Date time when the issue was created. Available only for admins and project owners.'
Bob Van Landuyt's avatar
Bob Van Landuyt committed
166
        optional :merge_request_to_resolve_discussions_of, type: Integer,
Robert Schilling's avatar
Robert Schilling committed
167
                                                           desc: 'The IID of a merge request for which to resolve discussions'
168
        optional :discussion_to_resolve, type: String,
Bob Van Landuyt's avatar
Bob Van Landuyt committed
169
                                         desc: 'The ID of a discussion to resolve, also pass `merge_request_to_resolve_discussions_of`'
Robert Schilling's avatar
Robert Schilling committed
170 171
        use :issue_params
      end
172
      post ':id/issues' do
173 174
        Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42320')

175 176
        authorize! :create_issue, user_project

Robert Schilling's avatar
Robert Schilling committed
177 178 179 180
        # Setting created_at time only allowed for admins and project owners
        unless current_user.admin? || user_project.owner == current_user
          params.delete(:created_at)
        end
181

Robert Schilling's avatar
Robert Schilling committed
182
        issue_params = declared_params(include_missing: false)
183

184 185
        issue_params = convert_parameters_from_legacy_format(issue_params)

Robert Schilling's avatar
Robert Schilling committed
186 187 188
        issue = ::Issues::CreateService.new(user_project,
                                            current_user,
                                            issue_params.merge(request: request, api: true)).execute
189

190
        if issue.spam?
191 192
          render_api_error!({ error: 'Spam detected' }, 400)
        end
193

194
        if issue.valid?
195
          present issue, with: Entities::Issue, current_user: current_user, project: user_project
196
        else
197
          render_validation_error!(issue)
Nihad Abbasov's avatar
Nihad Abbasov committed
198 199 200
        end
      end

201 202 203 204
      desc 'Update an existing issue' do
        success Entities::Issue
      end
      params do
205
        requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
Robert Schilling's avatar
Robert Schilling committed
206 207 208
        optional :title, type: String, desc: 'The title of an issue'
        optional :updated_at, type: DateTime,
                              desc: 'Date time when the issue was updated. Available only for admins and project owners.'
209
        optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue'
Robert Schilling's avatar
Robert Schilling committed
210
        use :issue_params
211
        at_least_one_of :title, :description, :assignee_ids, :assignee_id, :milestone_id,
212
                        :labels, :created_at, :due_date, :confidential, :state_event,
213
                        :weight, :discussion_locked
214
      end
215
      put ':id/issues/:issue_iid' do
216 217
        Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42322')

218
        issue = user_project.issues.find_by!(iid: params.delete(:issue_iid))
219
        authorize! :update_issue, issue
220

Robert Schilling's avatar
Robert Schilling committed
221 222 223 224
        # Setting created_at time only allowed for admins and project owners
        unless current_user.admin? || user_project.owner == current_user
          params.delete(:updated_at)
        end
225

226 227
        update_params = declared_params(include_missing: false).merge(request: request, api: true)

228 229
        update_params = convert_parameters_from_legacy_format(update_params)

Robert Schilling's avatar
Robert Schilling committed
230 231
        issue = ::Issues::UpdateService.new(user_project,
                                            current_user,
232 233 234
                                            update_params).execute(issue)

        render_spam_error! if issue.spam?
235

236
        if issue.valid?
237
          present issue, with: Entities::Issue, current_user: current_user, project: user_project
238
        else
239
          render_validation_error!(issue)
Nihad Abbasov's avatar
Nihad Abbasov committed
240 241 242
        end
      end

Robert Schilling's avatar
Robert Schilling committed
243 244 245 246
      desc 'Move an existing issue' do
        success Entities::Issue
      end
      params do
247
        requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
Robert Schilling's avatar
Robert Schilling committed
248 249
        requires :to_project_id, type: Integer, desc: 'The ID of the new project'
      end
250
      post ':id/issues/:issue_iid/move' do
251 252
        Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42323')

253
        issue = user_project.issues.find_by(iid: params[:issue_iid])
Robert Schilling's avatar
Robert Schilling committed
254
        not_found!('Issue') unless issue
255

Robert Schilling's avatar
Robert Schilling committed
256 257
        new_project = Project.find_by(id: params[:to_project_id])
        not_found!('Project') unless new_project
258 259 260

        begin
          issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project)
261
          present issue, with: Entities::Issue, current_user: current_user, project: user_project
262 263 264 265 266
        rescue ::Issues::MoveService::MoveError => error
          render_api_error!(error.message, 400)
        end
      end

Robert Schilling's avatar
Robert Schilling committed
267 268
      desc 'Delete a project issue'
      params do
269
        requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
Robert Schilling's avatar
Robert Schilling committed
270
      end
271 272
      delete ":id/issues/:issue_iid" do
        issue = user_project.issues.find_by(iid: params[:issue_iid])
Robert Schilling's avatar
Robert Schilling committed
273
        not_found!('Issue') unless issue
274

275
        authorize!(:destroy_issue, issue)
276

277 278 279
        destroy_conditionally!(issue) do |issue|
          Issuable::DestroyService.new(user_project, current_user).execute(issue)
        end
Nihad Abbasov's avatar
Nihad Abbasov committed
280
      end
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295

      desc 'List merge requests closing issue'  do
        success Entities::MergeRequestBasic
      end
      params do
        requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
      end
      get ':id/issues/:issue_iid/closed_by' do
        issue = find_project_issue(params[:issue_iid])

        merge_request_ids = MergeRequestsClosingIssues.where(issue_id: issue).select(:merge_request_id)
        merge_requests = MergeRequestsFinder.new(current_user, project_id: user_project.id).execute.where(id: merge_request_ids)

        present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project
      end
296

297 298 299 300 301 302 303 304 305 306 307 308 309
      desc 'List participants for an issue'  do
        success Entities::UserBasic
      end
      params do
        requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
      end
      get ':id/issues/:issue_iid/participants' do
        issue = find_project_issue(params[:issue_iid])
        participants = ::Kaminari.paginate_array(issue.participants)

        present paginate(participants), with: Entities::UserBasic, current_user: current_user, project: user_project
      end

310 311 312 313 314 315 316 317 318 319 320
      desc 'Get the user agent details for an issue' do
        success Entities::UserAgentDetail
      end
      params do
        requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
      end
      get ":id/issues/:issue_iid/user_agent_detail" do
        authenticated_as_admin!

        issue = find_project_issue(params[:issue_iid])

321
        break not_found!('UserAgentDetail') unless issue.user_agent_detail
322

323
        present issue.user_agent_detail, with: Entities::UserAgentDetail
324
      end
Nihad Abbasov's avatar
Nihad Abbasov committed
325 326 327
    end
  end
end