issuable_finder.rb 10.7 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
#     author_id: integer
15 16 17 18
#     assignee_id: integer
#     search: string
#     label_name: string
#     sort: string
19
#     non_archived: boolean
20
#     iids: integer[]
Hiroyuki Sato's avatar
Hiroyuki Sato committed
21
#     my_reaction_emoji: string
22
#
23
class IssuableFinder
24
  include CreatedAtFilter
25

Douwe Maan's avatar
Douwe Maan committed
26
  NONE = '0'.freeze
27

28 29 30 31 32 33 34 35 36 37 38
  SCALAR_PARAMS = %i[
    assignee_id
    assignee_username
    author_id
    author_username
    authorized_only
    due_date
    group_id
    iids
    label_name
    milestone_title
39
    my_reaction_emoji
40 41 42 43 44 45 46
    non_archived
    project_id
    scope
    search
    sort
    state
  ].freeze
47
  ARRAY_PARAMS = { label_name: [], iids: [], assignee_username: [] }.freeze
48

49 50
  VALID_PARAMS = (SCALAR_PARAMS + [ARRAY_PARAMS]).freeze

51
  attr_accessor :current_user, :params
52

53
  def initialize(current_user, params = {})
54 55
    @current_user = current_user
    @params = params
56
  end
57

58
  def execute
59 60
    items = init_collection
    items = by_scope(items)
61
    items = by_created_at(items)
62 63 64 65
    items = by_state(items)
    items = by_group(items)
    items = by_search(items)
    items = by_assignee(items)
66
    items = by_author(items)
67
    items = by_due_date(items)
68
    items = by_non_archived(items)
69
    items = by_iids(items)
70 71
    items = by_milestone(items)
    items = by_label(items)
Hiroyuki Sato's avatar
Hiroyuki Sato committed
72
    items = by_my_reaction_emoji(items)
73 74 75

    # 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)
76
    sort(items)
77 78
  end

79 80 81 82 83 84 85 86
  def find(*params)
    execute.find(*params)
  end

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

87 88 89 90
  def row_count
    Gitlab::IssuablesCountForState.new(self).for_state_or_opened(params[:state])
  end

91 92 93 94 95 96
  # 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
97
    count_params = params.merge(state: nil, sort: nil)
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
    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
  end

116 117 118 119
  def find_by!(*params)
    execute.find_by!(*params)
  end

120 121 122
  def group
    return @group if defined?(@group)

123
    @group =
124 125
      if params[:group_id].present?
        Group.find(params[:group_id])
126
      else
127 128 129 130
        nil
      end
  end

131 132 133 134
  def project?
    params[:project_id].present?
  end

135 136 137
  def project
    return @project if defined?(@project)

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

141
    @project = project
142 143
  end

144
  def projects(items = nil)
145 146 147 148 149 150
    return @projects = project if project?

    projects =
      if current_user && params[:authorized_only].presence && !current_user_related?
        current_user.authorized_projects
      elsif group
151
        GroupProjectsFinder.new(group: group, current_user: current_user).execute
152
      else
153
        ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute
154
      end
155

156
    @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
157 158 159 160 161 162 163 164 165 166
  end

  def search
    params[:search].presence
  end

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

Douwe Maan's avatar
Douwe Maan committed
167
  def filter_by_no_milestone?
168 169 170
    milestones? && params[:milestone_title] == Milestone::None.title
  end

171 172 173 174
  def milestones
    return @milestones if defined?(@milestones)

    @milestones =
175
      if milestones?
Felipe Artur's avatar
Felipe Artur committed
176 177 178 179 180 181
        if project?
          group_id = project.group&.id
          project_id = project.id
        end

        group_id = group.id if group
182

Felipe Artur's avatar
Felipe Artur committed
183 184 185 186
        search_params =
          { title: params[:milestone_title], project_ids: project_id, group_ids: group_id }

        MilestonesFinder.new(search_params).execute
187
      else
188
        Milestone.none
189 190 191
      end
  end

192 193 194 195
  def labels?
    params[:label_name].present?
  end

Douwe Maan's avatar
Douwe Maan committed
196
  def filter_by_no_label?
197
    labels? && params[:label_name].include?(Label::None.title)
198 199
  end

Tap's avatar
Tap committed
200 201 202
  def labels
    return @labels if defined?(@labels)

203 204
    @labels =
      if labels? && !filter_by_no_label?
205
        LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute(skip_authorization: true)
206 207
      else
        Label.none
Tap's avatar
Tap committed
208 209 210
      end
  end

211
  def assignee_id?
Lin Jen-Shin's avatar
Lin Jen-Shin committed
212
    params[:assignee_id].present? && params[:assignee_id] != NONE
213 214
  end

215
  def assignee_username?
Lin Jen-Shin's avatar
Lin Jen-Shin committed
216
    params[:assignee_username].present? && params[:assignee_username] != NONE
217 218
  end

219
  def no_assignee?
220
    # Assignee_id takes precedence over assignee_username
221 222 223
    params[:assignee_id] == NONE || params[:assignee_username] == NONE
  end

224 225 226
  def assignee
    return @assignee if defined?(@assignee)

227
    @assignee =
Lin Jen-Shin's avatar
Lin Jen-Shin committed
228
      if assignee_id?
229
        User.find_by(id: params[:assignee_id])
Lin Jen-Shin's avatar
Lin Jen-Shin committed
230
      elsif assignee_username?
231
        User.find_by(username: params[:assignee_username])
232 233 234 235 236
      else
        nil
      end
  end

237
  def author_id?
Lin Jen-Shin's avatar
Lin Jen-Shin committed
238
    params[:author_id].present? && params[:author_id] != NONE
239 240
  end

241
  def author_username?
Lin Jen-Shin's avatar
Lin Jen-Shin committed
242
    params[:author_username].present? && params[:author_username] != NONE
243 244
  end

245
  def no_author?
246
    # author_id takes precedence over author_username
247 248 249
    params[:author_id] == NONE || params[:author_username] == NONE
  end

250 251 252
  def author
    return @author if defined?(@author)

253
    @author =
254 255 256
      if author_id?
        User.find_by(id: params[:author_id])
      elsif author_username?
257
        User.find_by(username: params[:author_username])
258 259 260 261 262
      else
        nil
      end
  end

263 264
  private

265
  def init_collection
266
    klass.all
267 268 269
  end

  def by_scope(items)
270 271
    return items.none if current_user_related? && !current_user

Douwe Maan's avatar
Douwe Maan committed
272 273
    case params[:scope]
    when 'created-by-me', 'authored'
274
      items.where(author_id: current_user.id)
Douwe Maan's avatar
Douwe Maan committed
275
    when 'assigned-to-me'
276
      items.assigned_to(current_user)
277
    else
Douwe Maan's avatar
Douwe Maan committed
278
      items
279 280 281 282
    end
  end

  def by_state(items)
283 284 285 286 287 288 289
    case params[:state].to_s
    when 'closed'
      items.closed
    when 'merged'
      items.respond_to?(:merged) ? items.merged : items.closed
    when 'opened'
      items.opened
290
    else
291
      items
292 293 294 295
    end
  end

  def by_group(items)
296
    # Selection by group is already covered by `by_project` and `projects`
297 298 299 300
    items
  end

  def by_project(items)
301
    items =
302
      if project?
303 304 305
        items.of_projects(projects(items)).references_project
      elsif projects(items)
        items.merge(projects(items).reorder(nil)).join_project
306 307 308
      else
        items.none
      end
309 310 311 312 313

    items
  end

  def by_search(items)
314 315
    search ? items.full_search(search) : items
  end
316

317 318
  def by_iids(items)
    params[:iids].present? ? items.where(iid: params[:iids]) : items
319 320 321
  end

  def sort(items)
322 323
    # Ensure we always have an explicit sort order (instead of inheriting
    # multiple orders when combining ActiveRecord::Relation objects).
324
    params[:sort] ? items.sort(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc)
325 326 327
  end

  def by_assignee(items)
328 329
    if assignee
      items = items.where(assignee_id: assignee.id)
330 331
    elsif no_assignee?
      items = items.where(assignee_id: nil)
332 333
    elsif assignee_id? || assignee_username? # assignee not found
      items = items.none
334 335 336 337 338
    end

    items
  end

339
  def by_author(items)
340 341
    if author
      items = items.where(author_id: author.id)
342 343
    elsif no_author?
      items = items.where(author_id: nil)
344 345
    elsif author_id? || author_username? # author not found
      items = items.none
346 347 348 349 350
    end

    items
  end

tiagonbotelho's avatar
tiagonbotelho committed
351
  def filter_by_upcoming_milestone?
352
    params[:milestone_title] == Milestone::Upcoming.name
353 354
  end

355 356 357 358
  def filter_by_started_milestone?
    params[:milestone_title] == Milestone::Started.name
  end

359 360
  def by_milestone(items)
    if milestones?
Douwe Maan's avatar
Douwe Maan committed
361
      if filter_by_no_milestone?
362
        items = items.left_joins_milestones.where(milestone_id: [-1, nil])
tiagonbotelho's avatar
tiagonbotelho committed
363
      elsif filter_by_upcoming_milestone?
364
        upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items))
365
        items = items.left_joins_milestones.where(milestone_id: upcoming_ids)
366 367
      elsif filter_by_started_milestone?
        items = items.left_joins_milestones.where('milestones.start_date <= NOW()')
368
      else
369
        items = items.with_milestone(params[:milestone_title])
370 371 372 373 374 375
      end
    end

    items
  end

376
  def by_label(items)
377 378 379
    return items unless labels?

    items =
Douwe Maan's avatar
Douwe Maan committed
380
      if filter_by_no_label?
381
        items.without_label
382
      else
383
        items.with_label(label_names, params[:sort])
384
      end
385

386
    items
387
  end
388

Hiroyuki Sato's avatar
Hiroyuki Sato committed
389 390 391 392 393 394 395 396
  def by_my_reaction_emoji(items)
    if params[:my_reaction_emoji].present? && current_user
      items = items.awarded(current_user, params[:my_reaction_emoji])
    end

    items
  end

397 398 399
  def by_due_date(items)
    if due_date?
      if filter_by_no_due_date?
400 401
        items = items.without_due_date
      elsif filter_by_overdue?
Rémy Coutable's avatar
Rémy Coutable committed
402
        items = items.due_before(Date.today)
403
      elsif filter_by_due_this_week?
Rémy Coutable's avatar
Rémy Coutable committed
404
        items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week)
405
      elsif filter_by_due_this_month?
Rémy Coutable's avatar
Rémy Coutable committed
406
        items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month)
407 408
      end
    end
Rémy Coutable's avatar
Rémy Coutable committed
409

410 411
    items
  end
412

413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432
  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
433
  def label_names
Thijs Wouters's avatar
Thijs Wouters committed
434 435 436 437 438
    if labels?
      params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
    else
      []
    end
Tap's avatar
Tap committed
439 440
  end

441 442 443 444
  def by_non_archived(items)
    params[:non_archived].present? ? items.non_archived : items
  end

445 446 447
  def current_user_related?
    params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
  end
448
end