module API # Issues API class Issues < Grape::API before { authenticate! } helpers ::Gitlab::AkismetHelper helpers do def filter_issues_state(issues, state) case state when 'opened' then issues.opened when 'closed' then issues.closed else issues end end def filter_issues_labels(issues, labels) issues.includes(:labels).where('labels.title' => labels.split(',')) end def filter_issues_milestone(issues, milestone) issues.includes(:milestone).where('milestones.title' => milestone) end def create_spam_log(project, current_user, attrs) params = attrs.merge({ source_ip: client_ip(env), user_agent: user_agent(env), noteable_type: 'Issue', via_api: true }) ::CreateSpamLogService.new(project, current_user, params).execute end end resource :issues do # Get currently authenticated user's issues # # Parameters: # state (optional) - Return "opened" or "closed" issues # labels (optional) - Comma-separated list of label names # 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 /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 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? issues.reorder(issuable_order_by => issuable_sort) present paginate(issues), with: Entities::Issue, current_user: current_user end end resource :projects do # Get a list of project issues # # Parameters: # id (required) - The ID of a project # iid (optional) - Return the project issue having the given `iid` # 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 /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 # GET /projects/:id/issues?milestone=1.0.0 # GET /projects/:id/issues?milestone=1.0.0&state=closed # GET /issues?iid=42 get ":id/issues" do issues = user_project.issues.visible_to_user(current_user) issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil? unless params[:milestone].nil? issues = filter_issues_milestone(issues, params[:milestone]) end issues.reorder(issuable_order_by => issuable_sort) present paginate(issues), with: Entities::Issue, current_user: current_user end # Get a single project issue # # Parameters: # id (required) - The ID of a project # issue_id (required) - The ID of a project issue # Example Request: # GET /projects/:id/issues/:issue_id get ":id/issues/:issue_id" do @issue = user_project.issues.find(params[:issue_id]) not_found! unless can?(current_user, :read_issue, @issue) present @issue, with: Entities::Issue, current_user: current_user end # Create a new project issue # # Parameters: # 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 # milestone_id (optional) - The ID of a milestone to assign issue # labels (optional) - The labels of an issue # created_at (optional) - Date time string, ISO 8601 formatted # Example Request: # POST /projects/:id/issues post ":id/issues" do required_attributes! [:title] keys = [:title, :description, :assignee_id, :milestone_id] keys << :created_at if current_user.admin? || user_project.owner == current_user attrs = attributes_for_keys(keys) # Validate label names in advance if (errors = validate_label_params(params)).any? render_api_error!({ labels: errors }, 400) end project = user_project text = [attrs[:title], attrs[:description]].reject(&:blank?).join("\n") 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 if issue.valid? # 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 if params[:labels].present? issue.add_labels_by_names(params[:labels].split(',')) end present issue, with: Entities::Issue, current_user: current_user else render_validation_error!(issue) end end # Update an existing issue # # Parameters: # id (required) - The ID of a project # 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 # state_event (optional) - The state event of an issue (close|reopen) # updated_at (optional) - Date time string, ISO 8601 formatted # Example Request: # PUT /projects/:id/issues/:issue_id put ":id/issues/:issue_id" do issue = user_project.issues.find(params[:issue_id]) authorize! :update_issue, issue keys = [:title, :description, :assignee_id, :milestone_id, :state_event] keys << :updated_at if current_user.admin? || user_project.owner == current_user attrs = attributes_for_keys(keys) # Validate label names in advance if (errors = validate_label_params(params)).any? render_api_error!({ labels: errors }, 400) end issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue) if issue.valid? # 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 if params[:labels] && can?(current_user, :admin_issue, user_project) issue.remove_labels # Create and add labels to the new created issue issue.add_labels_by_names(params[:labels].split(',')) end present issue, with: Entities::Issue, current_user: current_user else render_validation_error!(issue) end end # Move an existing issue # # Parameters: # 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 # Example Request: # POST /projects/:id/issues/:issue_id/move post ':id/issues/:issue_id/move' do required_attributes! [:to_project_id] issue = user_project.issues.find(params[:issue_id]) new_project = Project.find(params[:to_project_id]) begin issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project) present issue, with: Entities::Issue, current_user: current_user rescue ::Issues::MoveService::MoveError => error render_api_error!(error.message, 400) end end # # Delete a project issue # # Parameters: # id (required) - The ID of a project # issue_id (required) - The ID of a project issue # Example Request: # DELETE /projects/:id/issues/:issue_id delete ":id/issues/:issue_id" do issue = user_project.issues.find_by(id: params[:issue_id]) authorize!(:destroy_issue, issue) issue.destroy end # Subscribes to a project issue # # Parameters: # id (required) - The ID of a project # issue_id (required) - The ID of a project issue # Example Request: # POST /projects/:id/issues/:issue_id/subscription post ':id/issues/:issue_id/subscription' do issue = user_project.issues.find(params[:issue_id]) if issue.subscribed?(current_user) not_modified! else issue.toggle_subscription(current_user) present issue, with: Entities::Issue, current_user: current_user end end # Unsubscribes from a project issue # # Parameters: # id (required) - The ID of a project # issue_id (required) - The ID of a project issue # Example Request: # DELETE /projects/:id/issues/:issue_id/subscription delete ':id/issues/:issue_id/subscription' do issue = user_project.issues.find(params[:issue_id]) if issue.subscribed?(current_user) issue.unsubscribe(current_user) present issue, with: Entities::Issue, current_user: current_user else not_modified! end end end end end