issuable_actions.rb 6.44 KB
Newer Older
1 2
# frozen_string_literal: true

3 4
module IssuableActions
  extend ActiveSupport::Concern
5
  include Gitlab::Utils::StrongMemoize
6 7 8

  included do
    before_action :authorize_destroy_issuable!, only: :destroy
9
    before_action :authorize_admin_issuable!, only: :bulk_update
10
    before_action only: :show do
11
      push_frontend_feature_flag(:reply_to_individual_notes, default_enabled: true)
12
    end
13 14
  end

15 16 17 18 19 20 21 22 23 24 25 26 27
  def permitted_keys
    [
      :issuable_ids,
      :assignee_id,
      :milestone_id,
      :state_event,
      :subscription_event,
      label_ids: [],
      add_label_ids: [],
      remove_label_ids: []
    ]
  end

28 29
  def show
    respond_to do |format|
30 31 32 33
      format.html do
        @issuable_sidebar = serializer.represent(issuable, serializer: 'sidebar') # rubocop:disable Gitlab/ModuleWithInstanceVariables
      end

34 35 36 37 38 39 40
      format.json do
        render json: serializer.represent(issuable, serializer: params[:serializer])
      end
    end
  end

  def update
41
    @issuable = update_service.execute(issuable) # rubocop:disable Gitlab/ModuleWithInstanceVariables
42 43
    respond_to do |format|
      format.html do
44
        recaptcha_check_if_spammable { render :edit }
45 46 47
      end

      format.json do
48
        recaptcha_check_if_spammable(false) { render_entity_json }
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
      end
    end

  rescue ActiveRecord::StaleObjectError
    render_conflict_response
  end

  def realtime_changes
    Gitlab::PollingInterval.set_header(response, interval: 3_000)

    response = {
      title: view_context.markdown_field(issuable, :title),
      title_text: issuable.title,
      description: view_context.markdown_field(issuable, :description),
      description_text: issuable.description,
64 65
      task_status: issuable.task_status,
      lock_version: issuable.lock_version
66 67 68
    }

    if issuable.edited?
69
      response[:updated_at] = issuable.last_edited_at.to_time.iso8601
70 71 72 73 74 75 76
      response[:updated_by_name] = issuable.last_edited_by.name
      response[:updated_by_path] = user_path(issuable.last_edited_by)
    end

    render json: response
  end

77
  def destroy
Valery Sizov's avatar
Valery Sizov committed
78
    Issuable::DestroyService.new(issuable.project, current_user).execute(issuable)
79

80
    name = issuable.human_class_name
81
    flash[:notice] = "The #{name} was successfully deleted."
82
    index_path = polymorphic_path([parent, issuable.class])
83 84 85 86 87

    respond_to do |format|
      format.html { redirect_to index_path }
      format.json do
        render json: {
88
          web_url: index_path
89 90 91
        }
      end
    end
92 93
  end

94 95 96 97 98 99 100
  def bulk_update
    result = Issuable::BulkUpdateService.new(project, current_user, bulk_update_params).execute(resource_name)
    quantity = result[:count]

    render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" }
  end

101
  # rubocop: disable CodeReuse/ActiveRecord
102
  def discussions
103
    notes = issuable.discussion_notes
104
      .inc_relations_for_view
105
      .with_notes_filter(notes_filter)
106 107 108
      .includes(:noteable)
      .fresh

109 110 111 112
    if notes_filter != UserPreference::NOTES_FILTERS[:only_comments]
      notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user).execute(notes)
    end

113 114 115 116 117
    notes = prepare_notes_for_rendering(notes)
    notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }

    discussions = Discussion.build_collection(notes, issuable)

118
    render json: discussion_serializer.represent(discussions, context: self)
119
  end
120
  # rubocop: enable CodeReuse/ActiveRecord
121

122 123
  private

124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
  def notes_filter
    strong_memoize(:notes_filter) do
      notes_filter_param = params[:notes_filter]&.to_i

      # GitLab Geo does not expect database UPDATE or INSERT statements to happen
      # on GET requests.
      # This is just a fail-safe in case notes_filter is sent via GET request in GitLab Geo.
      if Gitlab::Database.read_only?
        notes_filter_param || current_user&.notes_filter_for(issuable)
      else
        notes_filter = current_user&.set_notes_filter(notes_filter_param, issuable) || notes_filter_param

        # We need to invalidate the cache for polling notes otherwise it will
        # ignore the filter.
        # The ideal would be to invalidate the cache for each user.
        issuable.expire_note_etag_cache if notes_filter_updated?

        notes_filter
      end
    end
  end

  def notes_filter_updated?
    current_user&.user_preference&.previous_changes&.any?
  end

150 151 152 153
  def discussion_serializer
    DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user, note_entity: ProjectNoteEntity)
  end

154
  def recaptcha_check_if_spammable(should_redirect = true, &block)
Lin Jen-Shin's avatar
Lin Jen-Shin committed
155
    return yield unless issuable.is_a? Spammable
156 157 158 159

    recaptcha_check_with_fallback(should_redirect, &block)
  end

160 161 162
  def render_conflict_response
    respond_to do |format|
      format.html do
163
        @conflict = true # rubocop:disable Gitlab/ModuleWithInstanceVariables
164 165 166 167 168 169 170
        render :edit
      end

      format.json do
        render json: {
          errors: [
            "Someone edited this #{issuable.human_class_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."
171
          ]
Lin Jen-Shin's avatar
Lin Jen-Shin committed
172
        }, status: :conflict
173 174 175 176
      end
    end
  end

177
  def authorize_destroy_issuable!
178
    unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
179 180 181
      return access_denied!
    end
  end
182 183

  def authorize_admin_issuable!
184
    unless can?(current_user, :"admin_#{resource_name}", @project) # rubocop:disable Gitlab/ModuleWithInstanceVariables
185 186 187 188
      return access_denied!
    end
  end

189 190 191 192
  def authorize_update_issuable!
    render_404 unless can?(current_user, :"update_#{resource_name}", issuable)
  end

193
  def bulk_update_params
194
    permitted_keys_array = permitted_keys.dup
195 196

    if resource_name == 'issue'
197
      permitted_keys_array << { assignee_ids: [] }
198
    else
199
      permitted_keys_array.unshift(:assignee_id)
200 201
    end

202
    params.require(:update).permit(permitted_keys_array)
203 204 205 206 207
  end

  def resource_name
    @resource_name ||= controller_name.singularize
  end
208

209
  # rubocop:disable Gitlab/ModuleWithInstanceVariables
210 211 212 213 214 215 216
  def render_entity_json
    if @issuable.valid?
      render json: serializer.represent(@issuable)
    else
      render json: { errors: @issuable.errors.full_messages }, status: :unprocessable_entity
    end
  end
217
  # rubocop:enable Gitlab/ModuleWithInstanceVariables
218 219 220 221 222 223 224 225

  def serializer
    raise NotImplementedError
  end

  def update_service
    raise NotImplementedError
  end
226 227

  def parent
228
    @project || @group # rubocop:disable Gitlab/ModuleWithInstanceVariables
229
  end
230
end