issues.rb 10.6 KB
Newer Older
1
module API
Nihad Abbasov's avatar
Nihad Abbasov committed
2 3 4 5
  # Issues API
  class Issues < Grape::API
    before { authenticate! }

6 7
    helpers ::Gitlab::AkismetHelper

jubianchi's avatar
jubianchi committed
8
    helpers do
9
      def filter_issues_state(issues, state)
jubianchi's avatar
jubianchi committed
10
        case state
11 12
        when 'opened' then issues.opened
        when 'closed' then issues.closed
13
        else issues
jubianchi's avatar
jubianchi committed
14 15
        end
      end
jubianchi's avatar
jubianchi committed
16 17

      def filter_issues_labels(issues, labels)
18 19 20 21 22
        issues.includes(:labels).where('labels.title' => labels.split(','))
      end

      def filter_issues_milestone(issues, milestone)
        issues.includes(:milestone).where('milestones.title' => milestone)
jubianchi's avatar
jubianchi committed
23
      end
24 25

      def create_spam_log(project, current_user, attrs)
26
        params = attrs.merge({
27 28
          source_ip: client_ip(env),
          user_agent: user_agent(env),
29 30 31 32
          noteable_type: 'Issue',
          via_api: true
        })

33 34
        ::CreateSpamLogService.new(project, current_user, params).execute
      end
jubianchi's avatar
jubianchi committed
35 36
    end

Nihad Abbasov's avatar
Nihad Abbasov committed
37 38 39
    resource :issues do
      # Get currently authenticated user's issues
      #
jubianchi's avatar
jubianchi committed
40 41
      # Parameters:
      #   state (optional) - Return "opened" or "closed" issues
jubianchi's avatar
jubianchi committed
42
      #   labels (optional) - Comma-separated list of label names
43 44 45
      #   order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
      #   sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
      #
jubianchi's avatar
jubianchi committed
46
      # Example Requests:
Nihad Abbasov's avatar
Nihad Abbasov committed
47
      #   GET /issues
jubianchi's avatar
jubianchi committed
48 49
      #   GET /issues?state=opened
      #   GET /issues?state=closed
jubianchi's avatar
jubianchi committed
50 51 52
      #   GET /issues?labels=foo
      #   GET /issues?labels=foo,bar
      #   GET /issues?labels=foo,bar&state=opened
Nihad Abbasov's avatar
Nihad Abbasov committed
53
      get do
54
        issues = current_user.issues.inc_notes_with_associations
jubianchi's avatar
jubianchi committed
55 56
        issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
        issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
57
        issues.reorder(issuable_order_by => issuable_sort)
58
        present paginate(issues), with: Entities::Issue, current_user: current_user
Nihad Abbasov's avatar
Nihad Abbasov committed
59 60 61
      end
    end

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
    resource :groups do
      # Get a list of group issues
      #
      # Parameters:
      #   id (required) - The ID of a group
      #   state (optional) - Return "opened" or "closed" issues
      #   labels (optional) - Comma-separated list of label names
      #   milestone (optional) - Milestone title
      #   order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
      #   sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
      #
      # Example Requests:
      #   GET /groups/:id/issues
      #   GET /groups/:id/issues?state=opened
      #   GET /groups/:id/issues?state=closed
      #   GET /groups/:id/issues?labels=foo
      #   GET /groups/:id/issues?labels=foo,bar
      #   GET /groups/:id/issues?labels=foo,bar&state=opened
      #   GET /groups/:id/issues?milestone=1.0.0
      #   GET /groups/:id/issues?milestone=1.0.0&state=closed
      get ":id/issues" do
        group = find_group(params[:id])

        params[:state] ||= 'opened'
        params[:group_id] = group.id
        params[:milestone_title] = params.delete(:milestone)
        params[:label_name] = params.delete(:labels)
        params[:sort] = "#{params.delete(:order_by)}_#{params.delete(:sort)}" if params[:order_by] && params[:sort]

        issues = IssuesFinder.new(current_user, params).execute

        present paginate(issues), with: Entities::Issue, current_user: current_user
      end
    end

Nihad Abbasov's avatar
Nihad Abbasov committed
97 98 99 100
    resource :projects do
      # Get a list of project issues
      #
      # Parameters:
101
      #   id (required) - The ID of a project
102
      #   iid (optional) - Return the project issue having the given `iid`
jubianchi's avatar
jubianchi committed
103
      #   state (optional) - Return "opened" or "closed" issues
jubianchi's avatar
jubianchi committed
104
      #   labels (optional) - Comma-separated list of label names
105
      #   milestone (optional) - Milestone title
106 107
      #   order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
      #   sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
jubianchi's avatar
jubianchi committed
108 109
      #
      # Example Requests:
Nihad Abbasov's avatar
Nihad Abbasov committed
110
      #   GET /projects/:id/issues
jubianchi's avatar
jubianchi committed
111 112
      #   GET /projects/:id/issues?state=opened
      #   GET /projects/:id/issues?state=closed
jubianchi's avatar
jubianchi committed
113 114 115
      #   GET /projects/:id/issues?labels=foo
      #   GET /projects/:id/issues?labels=foo,bar
      #   GET /projects/:id/issues?labels=foo,bar&state=opened
116 117
      #   GET /projects/:id/issues?milestone=1.0.0
      #   GET /projects/:id/issues?milestone=1.0.0&state=closed
118
      #   GET /issues?iid=42
Nihad Abbasov's avatar
Nihad Abbasov committed
119
      get ":id/issues" do
120
        issues = user_project.issues.inc_notes_with_associations.visible_to_user(current_user)
jubianchi's avatar
jubianchi committed
121 122
        issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
        issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
123
        issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil?
124

125 126 127
        unless params[:milestone].nil?
          issues = filter_issues_milestone(issues, params[:milestone])
        end
jubianchi's avatar
jubianchi committed
128

129
        issues.reorder(issuable_order_by => issuable_sort)
130
        present paginate(issues), with: Entities::Issue, current_user: current_user
Nihad Abbasov's avatar
Nihad Abbasov committed
131 132 133 134 135
      end

      # Get a single project issue
      #
      # Parameters:
136
      #   id (required) - The ID of a project
Nihad Abbasov's avatar
Nihad Abbasov committed
137 138 139 140
      #   issue_id (required) - The ID of a project issue
      # Example Request:
      #   GET /projects/:id/issues/:issue_id
      get ":id/issues/:issue_id" do
141
        @issue = find_project_issue(params[:issue_id])
142
        present @issue, with: Entities::Issue, current_user: current_user
Nihad Abbasov's avatar
Nihad Abbasov committed
143 144 145 146 147
      end

      # Create a new project issue
      #
      # Parameters:
148 149 150 151
      #   id (required)           - The ID of a project
      #   title (required)        - The title of an issue
      #   description (optional)  - The description of an issue
      #   assignee_id (optional)  - The ID of a user to assign issue
Nihad Abbasov's avatar
Nihad Abbasov committed
152
      #   milestone_id (optional) - The ID of a milestone to assign issue
153
      #   labels (optional)       - The labels of an issue
154
      #   created_at (optional)   - Date time string, ISO 8601 formatted
155
      #   due_date (optional)     - Date time string in the format YEAR-MONTH-DAY
Nihad Abbasov's avatar
Nihad Abbasov committed
156 157
      # Example Request:
      #   POST /projects/:id/issues
158
      post ':id/issues' do
159
        required_attributes! [:title]
160

161
        keys = [:title, :description, :assignee_id, :milestone_id, :due_date]
162 163
        keys << :created_at if current_user.admin? || user_project.owner == current_user
        attrs = attributes_for_keys(keys)
164

165
        # Validate label names in advance
166 167
        if (errors = validate_label_params(params)).any?
          render_api_error!({ labels: errors }, 400)
168 169
        end

170
        project = user_project
171
        text = [attrs[:title], attrs[:description]].reject(&:blank?).join("\n")
172 173 174 175 176 177 178

        if check_for_spam?(project, current_user) && is_spam?(env, current_user, text)
          create_spam_log(project, current_user, attrs)
          render_api_error!({ error: 'Spam detected' }, 400)
        end

        issue = ::Issues::CreateService.new(project, current_user, attrs).execute
179 180

        if issue.valid?
181 182
          # Find or create labels and attach to issue. Labels are valid because
          # we already checked its name, so there can't be an error here
183
          if params[:labels].present?
184
            issue.add_labels_by_names(params[:labels].split(','))
185 186
          end

187
          present issue, with: Entities::Issue, current_user: current_user
188
        else
189
          render_validation_error!(issue)
Nihad Abbasov's avatar
Nihad Abbasov committed
190 191 192 193 194 195
        end
      end

      # Update an existing issue
      #
      # Parameters:
196
      #   id (required) - The ID of a project
Nihad Abbasov's avatar
Nihad Abbasov committed
197 198 199 200 201 202
      #   issue_id (required) - The ID of a project issue
      #   title (optional) - The title of an issue
      #   description (optional) - The description of an issue
      #   assignee_id (optional) - The ID of a user to assign issue
      #   milestone_id (optional) - The ID of a milestone to assign issue
      #   labels (optional) - The labels of an issue
203
      #   state_event (optional) - The state event of an issue (close|reopen)
204
      #   updated_at (optional) - Date time string, ISO 8601 formatted
205
      #   due_date (optional)     - Date time string in the format YEAR-MONTH-DAY
Nihad Abbasov's avatar
Nihad Abbasov committed
206 207
      # Example Request:
      #   PUT /projects/:id/issues/:issue_id
208
      put ':id/issues/:issue_id' do
209
        issue = user_project.issues.find(params[:issue_id])
210
        authorize! :update_issue, issue
211
        keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date]
212 213
        keys << :updated_at if current_user.admin? || user_project.owner == current_user
        attrs = attributes_for_keys(keys)
214

215
        # Validate label names in advance
216 217
        if (errors = validate_label_params(params)).any?
          render_api_error!({ labels: errors }, 400)
218 219
        end

220
        issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue)
221

222
        if issue.valid?
223 224
          # Find or create labels and attach to issue. Labels are valid because
          # we already checked its name, so there can't be an error here
225
          if params[:labels] && can?(current_user, :admin_issue, user_project)
226
            issue.remove_labels
227 228
            # Create and add labels to the new created issue
            issue.add_labels_by_names(params[:labels].split(','))
229 230
          end

231
          present issue, with: Entities::Issue, current_user: current_user
232
        else
233
          render_validation_error!(issue)
Nihad Abbasov's avatar
Nihad Abbasov committed
234 235 236
        end
      end

237 238 239
      # Move an existing issue
      #
      # Parameters:
240 241 242
      #  id (required)            - The ID of a project
      #  issue_id (required)      - The ID of a project issue
      #  to_project_id (required) - The ID of the new project
243 244
      # Example Request:
      #   POST /projects/:id/issues/:issue_id/move
245 246
      post ':id/issues/:issue_id/move' do
        required_attributes! [:to_project_id]
247 248

        issue = user_project.issues.find(params[:issue_id])
249
        new_project = Project.find(params[:to_project_id])
250 251 252

        begin
          issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project)
253
          present issue, with: Entities::Issue, current_user: current_user
254 255 256 257 258 259
        rescue ::Issues::MoveService::MoveError => error
          render_api_error!(error.message, 400)
        end
      end

      #
260
      # Delete a project issue
Nihad Abbasov's avatar
Nihad Abbasov committed
261 262
      #
      # Parameters:
263
      #   id (required) - The ID of a project
Nihad Abbasov's avatar
Nihad Abbasov committed
264 265 266 267
      #   issue_id (required) - The ID of a project issue
      # Example Request:
      #   DELETE /projects/:id/issues/:issue_id
      delete ":id/issues/:issue_id" do
268
        issue = user_project.issues.find_by(id: params[:issue_id])
269

270
        authorize!(:destroy_issue, issue)
271
        issue.destroy
Nihad Abbasov's avatar
Nihad Abbasov committed
272 273 274 275
      end
    end
  end
end