issuable_finder.rb 9.39 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
#
20
class IssuableFinder
21
  NONE = '0'
22

23
  attr_accessor :current_user, :params
24

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

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

46 47 48 49 50 51 52 53
  def find(*params)
    execute.find(*params)
  end

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

54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
  # 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

80 81 82 83
  def find_by!(*params)
    execute.find_by!(*params)
  end

84 85 86
  def group
    return @group if defined?(@group)

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

95 96 97 98
  def project?
    params[:project_id].present?
  end

99 100 101
  def project
    return @project if defined?(@project)

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

105
    @project = project
106 107 108 109
  end

  def projects
    return @projects if defined?(@projects)
110 111 112 113 114 115 116 117 118 119
    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
120

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

  def search
    params[:search].presence
  end

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

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

136 137 138 139
  def milestones
    return @milestones if defined?(@milestones)

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

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

149 150 151 152
  def labels?
    params[:label_name].present?
  end

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

Tap's avatar
Tap committed
157 158 159
  def labels
    return @labels if defined?(@labels)

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

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

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

176 177 178 179
  def no_assignee?
    params[:assignee_id] == NONE || params[:assignee_username] == NONE
  end

180 181 182
  def assignee
    return @assignee if defined?(@assignee)

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

193
  def author_id?
Lin Jen-Shin's avatar
Lin Jen-Shin committed
194
    params[:author_id].present? && params[:author_id] != NONE
195 196
  end

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

201 202 203 204
  def no_author?
    params[:author_id] == NONE || params[:author_username] == NONE
  end

205 206 207
  def author
    return @author if defined?(@author)

208
    @author =
209 210 211
      if author_id?
        User.find_by(id: params[:author_id])
      elsif author_username?
212
        User.find_by(username: params[:author_username])
213 214 215 216 217
      else
        nil
      end
  end

218 219
  private

220
  def init_collection
221
    klass.all
222 223 224
  end

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

  def by_state(items)
236 237 238 239 240 241 242
    case params[:state].to_s
    when 'closed'
      items.closed
    when 'merged'
      items.respond_to?(:merged) ? items.merged : items.closed
    when 'opened'
      items.opened
243
    else
244
      items
245 246 247 248
    end
  end

  def by_group(items)
249
    # Selection by group is already covered by `by_project` and `projects`
250 251 252 253
    items
  end

  def by_project(items)
254
    items =
255 256 257 258
      if project?
        items.of_projects(projects).references_project
      elsif projects
        items.merge(projects.reorder(nil)).join_project
259 260 261
      else
        items.none
      end
262 263 264 265 266

    items
  end

  def by_search(items)
barthc's avatar
barthc committed
267 268 269 270 271 272 273 274
    if search
      items =
        if search =~ iid_pattern
          items.where(iid: $~[:iid])
        else
          items.full_search(search)
        end
    end
275 276 277 278 279

    items
  end

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

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

    items
  end

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

    items
  end

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

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

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

    items
  end

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

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

346
    items
347
  end
348

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

362 363
    items
  end
364

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

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

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