openSUSE/open-build-service

View on GitHub
src/api/app/controllers/webui/request_controller.rb

Summary

Maintainability
C
1 day
Test Coverage
D
63%
class Webui::RequestController < Webui::WebuiController
  include Webui::NotificationsHandler
  include Webui::RequestsFilter
  include BuildNewComment

  helper 'webui/package'

  before_action :require_login,
                except: %i[show beta_show sourcediff diff request_action request_action_changes inline_comment build_results rpm_lint changes mentioned_issues]
  # requests do not really add much value for our page rank :)
  before_action :lockout_spiders
  before_action :require_request,
                only: %i[changerequest show beta_show request_action request_action_changes inline_comment build_results rpm_lint
                         changes mentioned_issues chart_build_results complete_build_results]
  before_action :set_actions, only: %i[inline_comment beta_show build_results rpm_lint changes mentioned_issues chart_build_results complete_build_results request_action_changes],
                              if: -> { Flipper.enabled?(:request_show_redesign, User.session) }
  before_action :set_actions_deprecated, only: [:show]
  before_action :build_results_data, only: [:beta_show], if: -> { Flipper.enabled?(:request_show_redesign, User.session) }
  before_action :set_action, only: %i[inline_comment beta_show build_results rpm_lint changes mentioned_issues],
                             if: -> { Flipper.enabled?(:request_show_redesign, User.session) }
  before_action :set_influxdb_data_request_actions, only: %i[beta_show build_results rpm_lint changes mentioned_issues],
                                                    if: -> { Flipper.enabled?(:request_show_redesign, User.session) }
  before_action :set_superseded_request, only: %i[show beta_show request_action request_action_changes build_results rpm_lint changes mentioned_issues]
  before_action :check_ajax, only: :sourcediff
  before_action :prepare_request_data, only: %i[beta_show build_results rpm_lint changes mentioned_issues],
                                       if: -> { Flipper.enabled?(:request_show_redesign, User.session) }
  before_action :cache_diff_data, only: %i[beta_show build_results rpm_lint changes mentioned_issues],
                                  if: -> { Flipper.enabled?(:request_show_redesign, User.session) }
  before_action :check_beta_user_redirect, only: %i[beta_show build_results rpm_lint changes mentioned_issues]

  after_action :verify_authorized, only: [:create]

  def beta_show
    @history_elements = @bs_request.history_elements.includes(:user)
    @active_tab = 'conversation'
  end

  # TODO: Remove this once request_show_redesign is rolled out
  def show
    redirect_to request_beta_show_path(params[:number], params[:request_action_id]) if Flipper.enabled?(:request_show_redesign, User.session)
    @diff_limit = params[:full_diff] ? 0 : nil
    @is_author = @bs_request.creator == User.possibly_nobody.login

    @is_target_maintainer = @bs_request.is_target_maintainer?(User.session)
    @can_handle_request = @bs_request.state.in?(%i[new review declined]) && (@is_target_maintainer || @is_author)

    @history = @bs_request.history_elements.includes(:user)

    # retrieve a list of all package maintainers that are assigned to at least one target package
    @package_maintainers = target_package_maintainers

    # search for a project, where the user is not a package maintainer but a project maintainer and show
    # a hint if that package has some package maintainers (issue#1970)
    @show_project_maintainer_hint = !@package_maintainers.empty? && @package_maintainers.exclude?(User.session) && any_project_maintained_by_current_user?
    @comments = @bs_request.comments
    @comment = Comment.new

    @current_notification = handle_notification

    @actions = @bs_request.webui_actions(filelimit: @diff_limit, tarlimit: @diff_limit, diff_to_superseded: @diff_to_superseded, diffs: false)
    @action = @actions.first
    @active = @action[:name]
    # TODO: this is the last instance of the @not_full_diff variable in the request scope, once request_workflow_redesign beta is rolled out,
    # let's get rid of this variable and also of `BsRequest.truncated_diffs` as it is no longer used anywhere else
    # print a hint that the diff is not fully shown (this only needs to be verified for submit actions)
    @not_full_diff = BsRequest.truncated_diffs?(@actions)

    reviews = @bs_request.reviews.where(state: 'new')
    user = User.session # might be nil
    @my_open_reviews = reviews.select { |review| review.matches_user?(user) }.reject(&:staging_project?)
    @can_add_reviews = @bs_request.state.in?(%i[new review]) && (@is_author || @is_target_maintainer || @my_open_reviews.present?)

    respond_to do |format|
      format.html
      format.js { render_request_update }
    end
  end

  def create
    @bs_request = BsRequest.new(bs_request_params)
    @bs_request.set_add_revision if params.key?(:add_revision)
    authorize @bs_request, :create?

    begin
      @bs_request.save!
      redirect_to request_show_path(@bs_request.number)
      return
    # FIXME: Use validations in the model instead of raising whenever something is wrong
    rescue BsRequestAction::MissingAction
      flash[:error] = 'Unable to submit, sources are unchanged'
    rescue Project::Errors::UnknownObjectError
      flash[:error] = "Unable to submit. The project '#{elide(params[:project_name])}' was not found"
    rescue APIError, ActiveRecord::RecordInvalid => e
      flash[:error] = e.message
    rescue Backend::Error => e
      flash[:error] = e.summary
    end

    if params.key?(:package_name)
      redirect_to(package_show_path(params[:project_name], params[:package_name]))
    else
      redirect_to(project_show_path(params[:project_name]))
    end
  end

  def add_reviewer
    request = BsRequest.find_by_number(params[:number])
    if request.nil?
      flash[:error] = "Unable to add review to request with id '#{params[:number]}': the request was not found."
    else
      begin
        request.addreview(addreview_opts)
      rescue BsRequestPermissionCheck::AddReviewNotPermitted
        flash[:error] = "Not permitted to add a review to '#{params[:number]}'"
      rescue ActiveRecord::RecordInvalid, APIError => e
        flash[:error] = "Unable to add review to request with id '#{params[:number]}': #{e.message}"
      end
    end
    redirect_to controller: :request, action: 'show', number: params[:number]
  end

  def modify_review_set_request
    review_params = params.slice(:comment, :review_id)
    review = Review.find_by(id: review_params[:review_id])
    unless review
      flash[:error] = 'Unable to load review'
      return review_params, nil
    end
    review_params[:by_package] = review.by_package
    review_params[:by_project] = review.by_project
    review_params[:by_user] = review.by_user
    review_params[:by_group] = review.by_group
    [review_params, review.bs_request]
  end

  def modify_review
    review_params, request = modify_review_set_request
    if request.nil?
      flash[:error] = 'Unable to load request'
      redirect_back_or_to user_path(User.session)
      return
    elsif !new_state.in?(%w[accepted declined])
      flash[:error] = 'Unknown state to set'
    else
      begin
        request.permission_check_change_review!(review_params)
        request.change_review_state(new_state, review_params)
        flash[:success] = 'Successfully submitted review'
      rescue BsRequestPermissionCheck::ReviewChangeStateNoPermission => e
        flash[:error] = "Not permitted to change review state: #{e.message}"
      rescue APIError => e
        flash[:error] = "Unable changing review state: #{e.message}"
      end
    end

    redirect_to request_show_path(number: request)
  end

  # TODO: Remove this once request_show_redesign is rolled out
  def request_action
    @diff_limit = params[:full_diff] ? 0 : nil
    @index = params[:index].to_i
    @actions = @bs_request.webui_actions(filelimit: @diff_limit, tarlimit: @diff_limit, diff_to_superseded: @diff_to_superseded, diffs: true,
                                         action_id: params['id'].to_i, cacheonly: 1)
    @action = @actions.find { |action| action[:id] == params['id'].to_i }
    @active = @action[:name]

    if @action[:diff_not_cached]
      bs_request_action = BsRequestAction.find(@action[:id])
      job = Delayed::Job.where('handler LIKE ?', "%job_class: BsRequestActionWebuiInfosJob%#{bs_request_action.to_global_id.uri}%").count
      BsRequestActionWebuiInfosJob.perform_later(bs_request_action) if job.zero?
    end

    respond_to do |format|
      format.js
    end
  end

  def request_action_changes
    return head :unauthorized unless @actions

    @action = @actions.where(id: params['id'].to_i).first

    cache_diff_data

    respond_to do |format|
      format.js
    end
  end

  def sourcediff
    render partial: 'webui/shared/editor', locals: { text: params[:text],
                                                     mode: 'diff', style: { read_only: true },
                                                     height: 'auto', width: '750px',
                                                     no_border: true, uid: params[:uid] }
  end

  def changerequest
    changestate = (%w[accepted commented declined revoked new] & params.keys).last

    if changestate == 'commented'

      build_new_comment(@bs_request, body: params[:reason])

    elsif change_state(changestate, params)
      # TODO: Make this work for each submit action individually
      if params[:add_submitter_as_maintainer_0] # rubocop:disable Naming/VariableNumber
        if changestate == 'accepted'
          # split into project and package
          tprj, tpkg = params[:add_submitter_as_maintainer_0].split('_#_') # rubocop:disable Naming/VariableNumber
          target = if tpkg
                     Package.find_by_project_and_name(tprj, tpkg)
                   else
                     Project.find_by_name(tprj)
                   end
          # the request action type might be permitted in future, but that doesn't mean we
          # are allowed to modify the object
          target.add_maintainer(@bs_request.creator) if target.can_be_modified_by?(User.possibly_nobody)
        else
          flash[:error] = 'Will not add maintainer for not accepted requests'
        end
      end
      accept_request if changestate == 'accepted'
    end
    redirect_to(request_show_path(params[:number]))
  end

  def diff
    # just for compatibility. OBS 1.X used this route for show
    redirect_to action: :show, number: params[:number]
  end

  def list_small
    redirect_to(user_path(User.possibly_nobody)) && return unless request.xhr? # non ajax request

    requests = BsRequest.list(params)
    render partial: 'requests_small', locals: { requests: requests }
  end

  def set_bugowner_request
    params.require(:project)
    request = nil
    begin
      request = BsRequest.create!(
        description: params[:description], bs_request_actions: [BsRequestAction.new(request_action_attributes(:set_bugowner))]
      )
    rescue APIError => e
      flash[:error] = e.message
      redirect_to(controller: :package, action: :show, package: params[:package], project: params[:project]) && return if params[:package]

      redirect_to(controller: :project, action: :show, project: params[:project]) && return
    end
    redirect_to controller: :request, action: :show, number: request.number
  end

  def set_incident
    request = BsRequest.find_by_number(params[:number])
    if request.nil?
      flash[:error] = 'Unable to load request'
    elsif params[:incident_project].blank?
      flash[:error] = 'Unknown incident project to set'
    else
      begin
        request.setincident(params[:incident_project])
        flash[:success] = "Set target of request #{request.number} to incident #{params[:incident_project]}"
      rescue Project::UnknownObjectError => e
        flash[:error] = "Incident #{e.message} does not exist"
      rescue APIError => e
        flash[:error] = "Not able to set incident: #{e.message}"
      end
    end

    redirect_to action: 'show', number: params[:number]
  end

  def inline_comment
    @file_index = params[:diff_file_index]
    @line_number = params[:diff_line_number]
    @line = "diff_#{@file_index}_n#{@line_number}"
    @source_rev = params[:source_rev]
    @target_rev = params[:target_rev]
    respond_to do |format|
      format.js
    end
  end

  def build_results
    redirect_to request_show_path(params[:number], params[:request_action_id]) unless @action.tab_visibility.build

    @active_tab = 'build_results'
    @project = @staging_project || @action.source_project
    @buildable = @action.source_package || @project
  end

  def rpm_lint
    redirect_to request_show_path(params[:number], params[:request_action_id]) unless @action.tab_visibility.rpm_lint

    @active_tab = 'rpm_lint'
    @ajax_data = {}
    @ajax_data['project'] = @action.source_project if @action.source_project
    @ajax_data['package'] = @action.source_package if @action.source_package
    @ajax_data['is_staged_request'] = true if @staging_project.present?
  end

  def changes
    redirect_to request_show_path(params[:number], params[:request_action_id]) unless @action.tab_visibility.changes

    @active_tab = 'changes'
  end

  def mentioned_issues
    redirect_to request_show_path(params[:number], params[:request_action_id]) unless @action.tab_visibility.issues

    @active_tab = 'mentioned_issues'
  end

  def chart_build_results
    render partial: 'webui/request/chart_build_results', locals: { chart_build_results_data: build_results_data }
  end

  def complete_build_results
    filters = params.keys - %w[controller action number]
    render partial: 'webui/request/beta_show_tabs/build_status', locals: { build_results_data: build_results_data, bs_request_number: @bs_request.number, filters: filters }
  end

  private

  def redirect_to_tasks
    redirect_to my_tasks_path
  end

  def check_beta_user_redirect
    redirect_to request_show_path(params[:number], params[:request_action_id]) unless Flipper.enabled?(:request_show_redesign, User.session)
  end

  def addreview_opts
    opts = {}
    case params[:review_type]
    when 'review-user'
      opts[:by_user] = params[:review_user]
    when 'review-group'
      opts[:by_group] = params[:review_group]
    when 'review-project'
      opts[:by_project] = params[:review_project]
    when 'review-package'
      opts[:by_project] = params[:review_project]
      opts[:by_package] = params[:review_package]
    end
    opts[:comment] = params[:review_comment] if params[:review_comment]

    opts
  end

  def any_project_maintained_by_current_user?
    projects = @actions.select(:target_project).distinct.pluck(:target_project)
    maintainer_role = Role.find_by_title('maintainer')
    projects.any? { |project| Project.find_by_name(project).user_has_role?(User.possibly_nobody, maintainer_role) }
  end

  def new_state
    case params[:new_state]
    when 'Approve'
      'accepted'
    when 'Decline'
      'declined'
    end
  end

  def set_superseded_request
    return unless (@diff_to_superseded_id = params[:diff_to_superseded])

    @diff_to_superseded = @bs_request.superseding.find_by(number: @diff_to_superseded_id)
    return if @diff_to_superseded

    flash[:error] = "Request #{@diff_to_superseded_id} does not exist or is not superseded by request #{@bs_request.number}."
    nil
  end

  def require_request
    @bs_request = BsRequest.find_by!(number: params[:number])
  end

  def target_package_maintainers
    distinct_bs_request_actions = @actions.select(:target_project, :target_package).distinct
    distinct_bs_request_actions.flat_map do |action|
      Package.find_by_project_and_name(action.target_project, action.target_package).try(:maintainers)
    end.compact.uniq
  end

  def change_state(newstate, params)
    request = BsRequest.find_by_number(params[:number])
    if request.nil?
      flash[:error] = 'Unable to load request'
    else
      # FIXME: make force optional, it hides warnings!
      opts = {
        newstate: newstate,
        force: true,
        user: User.session.login,
        comment: params[:reason]
      }
      begin
        request.change_state(opts)
        flash[:success] = "Request #{newstate}!"
        return true
      rescue APIError => e
        flash[:error] = "Failed to change state: #{e.message}!"
        return false
      end
    end

    false
  end

  def accept_request
    flash[:success] = "Request #{params[:number]} accepted"

    # Check if we have to forward this request to other projects / packages
    params.keys.grep(/^forward.*/).each do |fwd|
      forward_request_to(fwd)
    end
  end

  def forward_request_to(fwd)
    # split off 'forward_' and split into project and package
    tgt_prj, tgt_pkg = params[fwd].split('_#_')
    begin
      forwarded_request = @bs_request.forward_to(project: tgt_prj, package: tgt_pkg, options: params.slice(:description))
    rescue APIError, ActiveRecord::RecordInvalid => e
      error_string = "Failed to forward BsRequest: #{@bs_request.number}, error: #{e}, params: #{params.inspect}"
      error_string << ", request: #{e.record.inspect}" if e.respond_to?(:record)
      Airbrake.notify(error_string)
      flash[:error] = "Unable to forward submit request: #{e.message}"
      return
    end

    target_link = ActionController::Base.helpers.link_to("#{tgt_prj} / #{tgt_pkg}", package_show_url(project: tgt_prj, package: tgt_pkg))
    request_link = ActionController::Base.helpers.link_to("request #{forwarded_request.number}", request_show_path(forwarded_request.number))
    flash[:success] += " and forwarded to #{target_link} (#{request_link})"
  end

  # Subcontroller is expected to implement #bs_request_params
  # Strong parameters for BsRequest with nested attributes for its bs_request_actions association
  def bs_request_params
    raise AbstractMethodCalled
  end

  # FIXME: We should rely on strong parameters, so implement `bs_request_params` in subcontrollers as explained above
  def request_action_attributes(type)
    opt = {}
    opt['target_project'] = params[:project]
    opt['target_package'] = params[:package]
    opt['source_project'] = params[:devel_project]
    opt['source_package'] = params[:devel_package] || params[:package]
    opt['target_repository'] = params[:repository]
    opt['person_name'] = params[:user] if params[:user].present?
    opt['group_name'] = params[:group] if params[:group].present?
    opt['role'] = params[:role]
    opt['type'] = type.to_s
    opt
  end

  def render_request_update
    render partial: 'update', locals: {
      is_target_maintainer: @is_target_maintainer,
      is_author: @is_author,
      bs_request: @bs_request,
      history: @history,
      can_add_reviews: @can_add_reviews,
      package_maintainers: @package_maintainers,
      can_handle_request: @can_handle_request,
      my_open_reviews: @my_open_reviews,
      show_project_maintainer_hint: @show_project_maintainer_hint,
      actions: @actions
    }
  end

  def set_actions
    @actions = @bs_request.bs_request_actions
  end

  # [DEPRECATED] TODO: remove once request_workflow_redesign beta is rolled out
  # This method exists in order to have a set_actions in before_action for non beta too
  def set_actions_deprecated
    set_actions
  end

  def build_results_data
    ActionBuildResultsService::ChartDataExtractor.new(actions: @actions).call
  end

  def set_action
    # If no specific request_action_id, take the first
    action_id = params[:request_action_id] || @actions.first.id
    @action = @actions.find(action_id)
  end

  def staging_status(request, target_project)
    return nil unless (staging_review = request.reviews.staging(target_project).last)

    if staging_review.for_project?
      staging_project = {
        name: staging_review.project.name[target_project.name.length + 1..],
        url: staging_workflow_staging_project_path(target_project.name, staging_review.project.name)
      }
    end

    {
      staging_project: staging_project,
      target_project_staging_url: staging_workflow_path(request.target_project_name)
    }
  end

  def cache_diff_data
    return unless @action.diff_not_cached({ diff_to_superseded: @diff_to_superseded })

    job = Delayed::Job.where('handler LIKE ?', "%job_class: BsRequestActionWebuiInfosJob%#{@action.to_global_id.uri}%").count
    BsRequestActionWebuiInfosJob.perform_later(@action) if job.zero?
  end

  def prepare_request_data
    @is_target_maintainer = @bs_request.is_target_maintainer?(User.session)
    @my_open_reviews = ReviewsFinder.new(@bs_request.reviews).open_reviews_for_user(User.session).reject(&:staging_project?)

    # Handling request actions
    @action ||= @actions.first
    action_index = @actions.index(@action)
    if action_index
      @prev_action = @actions[action_index - 1] unless action_index.zero?
      @next_action = @actions[action_index + 1] if action_index + 1 < @actions.length
    end

    target_project = Project.find_by_name(@bs_request.target_project_name)
    @request_reviews = @bs_request.reviews.includes(%i[user group]).for_non_staging_projects(target_project)
    @staging_status = staging_status(@bs_request, target_project) if Staging::Workflow.find_by(project: target_project)

    # Collecting all issues in a hash. Each key is the issue name and the value is a hash containing all the issue details.
    @issues = @action.webui_sourcediff({ diff_to_superseded: @diff_to_superseded, cacheonly: 1 }).reduce({}) { |accumulator, sourcediff| accumulator.merge(sourcediff.fetch('issues', {})) }

    # retrieve a list of all package maintainers that are assigned to at least one target package
    @package_maintainers = target_package_maintainers

    # retrieve a list of all project maintainers
    @project_maintainers = target_project&.maintainers || []

    # search for a project, where the user is not a package maintainer but a project maintainer and show
    # a hint if that package has some package maintainers (issue#1970)
    @show_project_maintainer_hint = !@package_maintainers.empty? && @package_maintainers.exclude?(User.session) && any_project_maintained_by_current_user?

    # Handling build results
    @staging_project = @bs_request.staging_project.name unless @bs_request.staging_project_id.nil?

    @current_notification = handle_notification
  end

  def set_influxdb_data_request_actions
    InfluxDB::Rails.current.tags = {
      bs_request_action_type: @action.class.name
    }
  end
end