issuable_finder.rb 9.52 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
mhasbini's avatar
mhasbini committed
19
#     iids: integer[]
20
#
21
class IssuableFinder
Douwe Maan's avatar
Douwe Maan committed
22
  NONE = '0'.freeze
23

24
  attr_accessor :current_user, :params
25

26
  def initialize(current_user, params = {})
27
28
    @current_user = current_user
    @params = params
29
  end
30

31
  def execute
32
33
    items = init_collection
    items = by_scope(items)
34
35
    items = by_state(items)
    items = by_group(items)
Marin Jankovski's avatar
Marin Jankovski committed
36
    items = by_project(items)
37
38
39
    items = by_search(items)
    items = by_milestone(items)
    items = by_assignee(items)
40
    items = by_author(items)
41
    items = by_label(items)
42
    items = by_due_date(items)
43
    items = by_non_archived(items)
mhasbini's avatar
mhasbini committed
44
    items = by_iids(items)
45
    sort(items)
46
47
  end

48
49
50
51
52
53
54
55
  def find(*params)
    execute.find(*params)
  end

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

56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
  # 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
    count_params = params.merge(state: nil, sort: nil)
    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

82
83
84
85
  def find_by!(*params)
    execute.find_by!(*params)
  end

86
87
88
  def group
    return @group if defined?(@group)

89
    @group =
90
91
      if params[:group_id].present?
        Group.find(params[:group_id])
92
      else
93
94
95
96
        nil
      end
  end

97
98
99
100
  def project?
    params[:project_id].present?
  end

101
102
103
  def project
    return @project if defined?(@project)

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

107
    @project = project
108
109
110
111
  end

  def projects
    return @projects if defined?(@projects)
112
113
114
115
116
117
118
119
120
121
    return @projects = project if project?

    projects =
      if current_user && params[:authorized_only].presence && !current_user_related?
        current_user.authorized_projects
      elsif group
        GroupProjectsFinder.new(group).execute(current_user)
      else
        ProjectsFinder.new.execute(current_user)
      end
122

123
    @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
124
125
126
127
128
129
130
131
132
133
  end

  def search
    params[:search].presence
  end

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

Douwe Maan's avatar
Douwe Maan committed
134
  def filter_by_no_milestone?
135
136
137
    milestones? && params[:milestone_title] == Milestone::None.title
  end

138
139
140
141
  def milestones
    return @milestones if defined?(@milestones)

    @milestones =
142
      if milestones?
143
        scope = Milestone.where(project_id: projects)
144
145

        scope.where(title: params[:milestone_title])
146
      else
147
        Milestone.none
148
149
150
      end
  end

151
152
153
154
  def labels?
    params[:label_name].present?
  end

Douwe Maan's avatar
Douwe Maan committed
155
  def filter_by_no_label?
156
    labels? && params[:label_name].include?(Label::None.title)
157
158
  end

Tap's avatar
Tap committed
159
160
161
  def labels
    return @labels if defined?(@labels)

162
163
    @labels =
      if labels? && !filter_by_no_label?
164
        LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute(skip_authorization: true)
165
166
      else
        Label.none
Tap's avatar
Tap committed
167
168
169
      end
  end

170
  def assignee_id?
Lin Jen-Shin's avatar
Lin Jen-Shin committed
171
    params[:assignee_id].present? && params[:assignee_id] != NONE
172
173
  end

174
  def assignee_username?
Lin Jen-Shin's avatar
Lin Jen-Shin committed
175
    params[:assignee_username].present? && params[:assignee_username] != NONE
176
177
  end

178
  def no_assignee?
Clement Ho's avatar
Clement Ho committed
179
    # Assignee_id takes precedence over assignee_username
180
181
182
    params[:assignee_id] == NONE || params[:assignee_username] == NONE
  end

183
184
185
  def assignee
    return @assignee if defined?(@assignee)

186
    @assignee =
Lin Jen-Shin's avatar
Lin Jen-Shin committed
187
      if assignee_id?
188
        User.find_by(id: params[:assignee_id])
Lin Jen-Shin's avatar
Lin Jen-Shin committed
189
      elsif assignee_username?
190
        User.find_by(username: params[:assignee_username])
191
192
193
194
195
      else
        nil
      end
  end

196
  def author_id?
Lin Jen-Shin's avatar
Lin Jen-Shin committed
197
    params[:author_id].present? && params[:author_id] != NONE
198
199
  end

200
  def author_username?
Lin Jen-Shin's avatar
Lin Jen-Shin committed
201
    params[:author_username].present? && params[:author_username] != NONE
202
203
  end

204
  def no_author?
Clement Ho's avatar
Clement Ho committed
205
    # author_id takes precedence over author_username
206
207
208
    params[:author_id] == NONE || params[:author_username] == NONE
  end

209
210
211
  def author
    return @author if defined?(@author)

212
    @author =
213
214
215
      if author_id?
        User.find_by(id: params[:author_id])
      elsif author_username?
216
        User.find_by(username: params[:author_username])
217
218
219
220
221
      else
        nil
      end
  end

222
223
  private

224
  def init_collection
225
    klass.all
226
227
228
  end

  def by_scope(items)
Douwe Maan's avatar
Douwe Maan committed
229
230
    case params[:scope]
    when 'created-by-me', 'authored'
231
      items.where(author_id: current_user.id)
Douwe Maan's avatar
Douwe Maan committed
232
    when 'assigned-to-me'
233
      items.where(assignee_id: current_user.id)
234
    else
Douwe Maan's avatar
Douwe Maan committed
235
      items
236
237
238
239
    end
  end

  def by_state(items)
240
241
242
243
244
245
246
    case params[:state].to_s
    when 'closed'
      items.closed
    when 'merged'
      items.respond_to?(:merged) ? items.merged : items.closed
    when 'opened'
      items.opened
247
    else
248
      items
249
250
251
252
    end
  end

  def by_group(items)
253
    # Selection by group is already covered by `by_project` and `projects`
254
255
256
257
    items
  end

  def by_project(items)
258
    items =
259
260
261
262
      if project?
        items.of_projects(projects).references_project
      elsif projects
        items.merge(projects.reorder(nil)).join_project
263
264
265
      else
        items.none
      end
266
267
268
269
270

    items
  end

  def by_search(items)
mhasbini's avatar
mhasbini committed
271
272
    search ? items.full_search(search) : items
  end
273

mhasbini's avatar
mhasbini committed
274
275
  def by_iids(items)
    params[:iids].present? ? items.where(iid: params[:iids]) : items
276
277
278
  end

  def sort(items)
279
280
    # Ensure we always have an explicit sort order (instead of inheriting
    # multiple orders when combining ActiveRecord::Relation objects).
281
    params[:sort] ? items.sort(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc)
282
283
284
  end

  def by_assignee(items)
285
286
    if assignee
      items = items.where(assignee_id: assignee.id)
287
288
    elsif no_assignee?
      items = items.where(assignee_id: nil)
289
290
    elsif assignee_id? || assignee_username? # assignee not found
      items = items.none
291
292
293
294
295
    end

    items
  end

296
  def by_author(items)
297
298
    if author
      items = items.where(author_id: author.id)
299
300
    elsif no_author?
      items = items.where(author_id: nil)
301
302
    elsif author_id? || author_username? # author not found
      items = items.none
303
304
305
306
307
    end

    items
  end

tiagonbotelho's avatar
tiagonbotelho committed
308
  def filter_by_upcoming_milestone?
309
    params[:milestone_title] == Milestone::Upcoming.name
tiagonbotelho's avatar
tiagonbotelho committed
310
311
  end

312
313
  def by_milestone(items)
    if milestones?
Douwe Maan's avatar
Douwe Maan committed
314
      if filter_by_no_milestone?
315
        items = items.left_joins_milestones.where(milestone_id: [-1, nil])
tiagonbotelho's avatar
tiagonbotelho committed
316
      elsif filter_by_upcoming_milestone?
317
        upcoming_ids = Milestone.upcoming_ids_by_projects(projects)
318
        items = items.left_joins_milestones.where(milestone_id: upcoming_ids)
319
      else
320
        items = items.with_milestone(params[:milestone_title])
321
322
323
324
325
326
327
328
329
330

        if projects
          items = items.where(milestones: { project_id: projects })
        end
      end
    end

    items
  end

331
  def by_label(items)
332
    if labels?
Douwe Maan's avatar
Douwe Maan committed
333
      if filter_by_no_label?
334
        items = items.without_label
335
      else
336
        items = items.with_label(label_names, params[:sort])
337

338
        if projects
339
          label_ids = LabelsFinder.new(current_user, project_ids: projects).execute(skip_authorization: true).select(:id)
340
          items = items.where(labels: { id: label_ids })
341
        end
342
      end
343
344
    end

345
    items
346
  end
347

348
349
350
  def by_due_date(items)
    if due_date?
      if filter_by_no_due_date?
351
352
        items = items.without_due_date
      elsif filter_by_overdue?
Rémy Coutable's avatar
Rémy Coutable committed
353
        items = items.due_before(Date.today)
354
      elsif filter_by_due_this_week?
Rémy Coutable's avatar
Rémy Coutable committed
355
        items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week)
356
      elsif filter_by_due_this_month?
Rémy Coutable's avatar
Rémy Coutable committed
357
        items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month)
358
359
      end
    end
Rémy Coutable's avatar
Rémy Coutable committed
360

361
362
    items
  end
363

364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
  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
384
  def label_names
Thijs Wouters's avatar
Thijs Wouters committed
385
386
387
388
389
    if labels?
      params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
    else
      []
    end
Tap's avatar
Tap committed
390
391
  end

392
393
394
395
  def by_non_archived(items)
    params[:non_archived].present? ? items.non_archived : items
  end

396
397
398
  def current_user_related?
    params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
  end
399
end