issuable_finder.rb 10.6 KB
Newer Older
1
# IssuableFinder
2 3 4 5 6 7 8 9
#
# Used to filter Issues and MergeRequests collections by set of params
#
# Arguments:
#   klass - actual class like Issue or MergeRequest
#   current_user - which user use
#   params:
#     scope: 'created-by-me' or 'assigned-to-me' or 'all'
10
#     state: 'opened' or 'closed' or 'all'
11 12
#     group_id: integer
#     project_id: integer
13
#     milestone_title: string
14 15 16 17
#     assignee_id: integer
#     search: string
#     label_name: string
#     sort: string
18
#     non_archived: boolean
19
#     iids: integer[]
20
#
21
class IssuableFinder
22
  include CreatedAtFilter
23

Douwe Maan's avatar
Douwe Maan committed
24
  NONE = '0'.freeze
25
  IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze
26

27
  attr_accessor :current_user, :params
28

29
  def initialize(current_user, params = {})
30 31
    @current_user = current_user
    @params = params
32
  end
33

34
  def execute
35 36
    items = init_collection
    items = by_scope(items)
37
    items = by_created_at(items)
38 39 40 41
    items = by_state(items)
    items = by_group(items)
    items = by_search(items)
    items = by_assignee(items)
42
    items = by_author(items)
43
    items = by_due_date(items)
44
    items = by_non_archived(items)
45
    items = by_iids(items)
46 47 48 49 50
    items = by_milestone(items)
    items = by_label(items)

    # Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far
    items = by_project(items)
51
    sort(items)
52 53
  end

54 55 56 57 58 59 60 61
  def find(*params)
    execute.find(*params)
  end

  def find_by(*params)
    execute.find_by(*params)
  end

62 63 64 65 66 67
  # We often get counts for each state by running a query per state, and
  # counting those results. This is typically slower than running one query
  # (even if that query is slower than any of the individual state queries) and
  # grouping and counting within that query.
  #
  def count_by_state
68
    count_params = params.merge(state: nil, sort: nil, for_counting: true)
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
    labels_count = label_names.any? ? label_names.count : 1
    finder = self.class.new(current_user, count_params)
    counts = Hash.new(0)

    # Searching by label includes a GROUP BY in the query, but ours will be last
    # because it is added last. Searching by multiple labels also includes a row
    # per issuable, so we have to count those in Ruby - which is bad, but still
    # better than performing multiple queries.
    #
    finder.execute.reorder(nil).group(:state).count.each do |key, value|
      counts[Array(key).last.to_sym] += value / labels_count
    end

    counts[:all] = counts.values.sum
    counts[:opened] += counts[:reopened]

    counts
  end

88 89 90 91
  def find_by!(*params)
    execute.find_by!(*params)
  end

92 93 94 95
  def state_counter_cache_key(state)
    Digest::SHA1.hexdigest(state_counter_cache_key_components(state).flatten.join('-'))
  end

96 97 98
  def group
    return @group if defined?(@group)

99
    @group =
100 101
      if params[:group_id].present?
        Group.find(params[:group_id])
102
      else
103 104 105 106
        nil
      end
  end

107 108 109 110
  def project?
    params[:project_id].present?
  end

111 112 113
  def project
    return @project if defined?(@project)

114 115
    project = Project.find(params[:project_id])
    project = nil unless Ability.allowed?(current_user, :"read_#{klass.to_ability_name}", project)
116

117
    @project = project
118 119
  end

120
  def projects(items = nil)
121 122 123 124 125 126
    return @projects = project if project?

    projects =
      if current_user && params[:authorized_only].presence && !current_user_related?
        current_user.authorized_projects
      elsif group
127
        GroupProjectsFinder.new(group: group, current_user: current_user).execute
128
      else
129
        ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute
130
      end
131

132
    @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
133 134 135 136 137 138 139 140 141 142
  end

  def search
    params[:search].presence
  end

  def milestones?
    params[:milestone_title].present?
  end

Douwe Maan's avatar
Douwe Maan committed
143
  def filter_by_no_milestone?
144 145 146
    milestones? && params[:milestone_title] == Milestone::None.title
  end

147 148 149 150
  def milestones
    return @milestones if defined?(@milestones)

    @milestones =
151
      if milestones?
Felipe Artur's avatar
Felipe Artur committed
152 153 154 155 156 157
        if project?
          group_id = project.group&.id
          project_id = project.id
        end

        group_id = group.id if group
158

Felipe Artur's avatar
Felipe Artur committed
159 160 161 162
        search_params =
          { title: params[:milestone_title], project_ids: project_id, group_ids: group_id }

        MilestonesFinder.new(search_params).execute
163
      else
164
        Milestone.none
165 166 167
      end
  end

168 169 170 171
  def labels?
    params[:label_name].present?
  end

Douwe Maan's avatar
Douwe Maan committed
172
  def filter_by_no_label?
173
    labels? && params[:label_name].include?(Label::None.title)
174 175
  end

Tap's avatar
Tap committed
176 177 178
  def labels
    return @labels if defined?(@labels)

179 180
    @labels =
      if labels? && !filter_by_no_label?
181
        LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute(skip_authorization: true)
182 183
      else
        Label.none
Tap's avatar
Tap committed
184 185 186
      end
  end

187
  def assignee_id?
Lin Jen-Shin's avatar
Lin Jen-Shin committed
188
    params[:assignee_id].present? && params[:assignee_id] != NONE
189 190
  end

191
  def assignee_username?
Lin Jen-Shin's avatar
Lin Jen-Shin committed
192
    params[:assignee_username].present? && params[:assignee_username] != NONE
193 194
  end

195
  def no_assignee?
Clement Ho's avatar
Clement Ho committed
196
    # Assignee_id takes precedence over assignee_username
197 198 199
    params[:assignee_id] == NONE || params[:assignee_username] == NONE
  end

200 201 202
  def assignee
    return @assignee if defined?(@assignee)

203
    @assignee =
Lin Jen-Shin's avatar
Lin Jen-Shin committed
204
      if assignee_id?
205
        User.find_by(id: params[:assignee_id])
Lin Jen-Shin's avatar
Lin Jen-Shin committed
206
      elsif assignee_username?
207
        User.find_by(username: params[:assignee_username])
208 209 210 211 212
      else
        nil
      end
  end

213
  def author_id?
Lin Jen-Shin's avatar
Lin Jen-Shin committed
214
    params[:author_id].present? && params[:author_id] != NONE
215 216
  end

217
  def author_username?
Lin Jen-Shin's avatar
Lin Jen-Shin committed
218
    params[:author_username].present? && params[:author_username] != NONE
219 220
  end

221
  def no_author?
Clement Ho's avatar
Clement Ho committed
222
    # author_id takes precedence over author_username
223 224 225
    params[:author_id] == NONE || params[:author_username] == NONE
  end

226 227 228
  def author
    return @author if defined?(@author)

229
    @author =
230 231 232
      if author_id?
        User.find_by(id: params[:author_id])
      elsif author_username?
233
        User.find_by(username: params[:author_username])
234 235 236 237 238
      else
        nil
      end
  end

239 240
  private

241
  def init_collection
242
    klass.all
243 244 245
  end

  def by_scope(items)
Douwe Maan's avatar
Douwe Maan committed
246 247
    case params[:scope]
    when 'created-by-me', 'authored'
248
      items.where(author_id: current_user.id)
Douwe Maan's avatar
Douwe Maan committed
249
    when 'assigned-to-me'
250
      items.assigned_to(current_user)
251
    else
Douwe Maan's avatar
Douwe Maan committed
252
      items
253 254 255 256
    end
  end

  def by_state(items)
257 258 259 260 261 262 263
    case params[:state].to_s
    when 'closed'
      items.closed
    when 'merged'
      items.respond_to?(:merged) ? items.merged : items.closed
    when 'opened'
      items.opened
264
    else
265
      items
266 267 268 269
    end
  end

  def by_group(items)
270
    # Selection by group is already covered by `by_project` and `projects`
271 272 273 274
    items
  end

  def by_project(items)
275
    items =
276
      if project?
277 278 279
        items.of_projects(projects(items)).references_project
      elsif projects(items)
        items.merge(projects(items).reorder(nil)).join_project
280 281 282
      else
        items.none
      end
283 284 285 286 287

    items
  end

  def by_search(items)
288 289
    search ? items.full_search(search) : items
  end
290

291 292
  def by_iids(items)
    params[:iids].present? ? items.where(iid: params[:iids]) : items
293 294 295
  end

  def sort(items)
296 297
    # Ensure we always have an explicit sort order (instead of inheriting
    # multiple orders when combining ActiveRecord::Relation objects).
298
    params[:sort] ? items.sort(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc)
299 300 301
  end

  def by_assignee(items)
302 303
    if assignee
      items = items.where(assignee_id: assignee.id)
304 305
    elsif no_assignee?
      items = items.where(assignee_id: nil)
306 307
    elsif assignee_id? || assignee_username? # assignee not found
      items = items.none
308 309 310 311 312
    end

    items
  end

313
  def by_author(items)
314 315
    if author
      items = items.where(author_id: author.id)
316 317
    elsif no_author?
      items = items.where(author_id: nil)
318 319
    elsif author_id? || author_username? # author not found
      items = items.none
320 321 322 323 324
    end

    items
  end

tiagonbotelho's avatar
tiagonbotelho committed
325
  def filter_by_upcoming_milestone?
326
    params[:milestone_title] == Milestone::Upcoming.name
327 328
  end

329 330 331 332
  def filter_by_started_milestone?
    params[:milestone_title] == Milestone::Started.name
  end

333 334
  def by_milestone(items)
    if milestones?
Douwe Maan's avatar
Douwe Maan committed
335
      if filter_by_no_milestone?
336
        items = items.left_joins_milestones.where(milestone_id: [-1, nil])
tiagonbotelho's avatar
tiagonbotelho committed
337
      elsif filter_by_upcoming_milestone?
338
        upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items))
339
        items = items.left_joins_milestones.where(milestone_id: upcoming_ids)
340 341
      elsif filter_by_started_milestone?
        items = items.left_joins_milestones.where('milestones.start_date <= NOW()')
342
      else
343
        items = items.with_milestone(params[:milestone_title])
344 345 346 347 348 349
      end
    end

    items
  end

350
  def by_label(items)
351
    if labels?
Douwe Maan's avatar
Douwe Maan committed
352
      if filter_by_no_label?
353
        items = items.without_label
354
      else
355
        items = items.with_label(label_names, params[:sort])
356
        items_projects = projects(items)
357

358 359
        if items_projects
          label_ids = LabelsFinder.new(current_user, project_ids: items_projects).execute(skip_authorization: true).select(:id)
360
          items = items.where(labels: { id: label_ids })
361
        end
362
      end
363 364
    end

365
    items
366
  end
367

368 369 370
  def by_due_date(items)
    if due_date?
      if filter_by_no_due_date?
371 372
        items = items.without_due_date
      elsif filter_by_overdue?
Rémy Coutable's avatar
Rémy Coutable committed
373
        items = items.due_before(Date.today)
374
      elsif filter_by_due_this_week?
Rémy Coutable's avatar
Rémy Coutable committed
375
        items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week)
376
      elsif filter_by_due_this_month?
Rémy Coutable's avatar
Rémy Coutable committed
377
        items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month)
378 379
      end
    end
Rémy Coutable's avatar
Rémy Coutable committed
380

381 382
    items
  end
383

384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
  def filter_by_no_due_date?
    due_date? && params[:due_date] == Issue::NoDueDate.name
  end

  def filter_by_overdue?
    due_date? && params[:due_date] == Issue::Overdue.name
  end

  def filter_by_due_this_week?
    due_date? && params[:due_date] == Issue::DueThisWeek.name
  end

  def filter_by_due_this_month?
    due_date? && params[:due_date] == Issue::DueThisMonth.name
  end

  def due_date?
    params[:due_date].present? && klass.column_names.include?('due_date')
  end

Tap's avatar
Tap committed
404
  def label_names
Thijs Wouters's avatar
Thijs Wouters committed
405 406 407 408 409
    if labels?
      params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
    else
      []
    end
Tap's avatar
Tap committed
410 411
  end

412 413 414 415
  def by_non_archived(items)
    params[:non_archived].present? ? items.non_archived : items
  end

416 417 418
  def current_user_related?
    params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
  end
419 420 421 422 423 424 425 426 427

  def state_counter_cache_key_components(state)
    opts = params.with_indifferent_access
    opts[:state] = state
    opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
    opts.delete_if { |_, value| value.blank? }

    ['issuables_count', klass.to_ability_name, opts.sort]
  end
428
end