Commit 4c1dc310 authored by Yorick Peterse's avatar Yorick Peterse

Clean up ActiveRecord code in TodosFinder

This refactors the TodosFinder finder according to the new code reuse
rules, as enforced by the CodeReuse cops. I also changed some of the
methods to use regular if statements, instead of assignments and/or
early returns. This results in a more natural flow when reading the
code, and it makes it harder to accidentally return the wrong result.
parent c616327c
...@@ -23,6 +23,8 @@ class TodosFinder ...@@ -23,6 +23,8 @@ class TodosFinder
NONE = '0'.freeze NONE = '0'.freeze
TODO_TYPES = Set.new(%w(Issue MergeRequest Epic)).freeze
attr_accessor :current_user, :params attr_accessor :current_user, :params
def initialize(current_user, params = {}) def initialize(current_user, params = {})
...@@ -72,14 +74,11 @@ class TodosFinder ...@@ -72,14 +74,11 @@ class TodosFinder
end end
def author def author
return @author if defined?(@author) strong_memoize(:author) do
@author =
if author? && params[:author_id] != NONE if author? && params[:author_id] != NONE
User.find(params[:author_id]) User.find(params[:author_id])
else
nil
end end
end
end end
def project? def project?
...@@ -91,17 +90,9 @@ class TodosFinder ...@@ -91,17 +90,9 @@ class TodosFinder
end end
def project def project
return @project if defined?(@project) strong_memoize(:project) do
Project.find_without_deleted(params[:project_id]) if project?
if project?
@project = Project.find(params[:project_id])
@project = nil if @project.pending_delete?
else
@project = nil
end end
@project
end end
def group def group
...@@ -111,7 +102,7 @@ class TodosFinder ...@@ -111,7 +102,7 @@ class TodosFinder
end end
def type? def type?
type.present? && %w(Issue MergeRequest Epic).include?(type) type.present? && TODO_TYPES.include?(type)
end end
def type def type
...@@ -119,77 +110,66 @@ class TodosFinder ...@@ -119,77 +110,66 @@ class TodosFinder
end end
def sort(items) def sort(items)
params[:sort] ? items.sort_by_attribute(params[:sort]) : items.order_id_desc if params[:sort]
items.sort_by_attribute(params[:sort])
else
items.order_id_desc
end
end end
# rubocop: disable CodeReuse/ActiveRecord
def by_action(items) def by_action(items)
if action? if action?
items = items.where(action: to_action_id) items.for_action(to_action_id)
else
items
end end
items
end end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def by_action_id(items) def by_action_id(items)
if action_id? if action_id?
items = items.where(action: action_id) items.for_action(action_id)
else
items
end end
items
end end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def by_author(items) def by_author(items)
if author? if author?
items = items.where(author_id: author.try(:id)) items.for_author(author)
else
items
end end
items
end end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def by_project(items) def by_project(items)
if project? if project?
items = items.where(project: project) items.for_project(project)
else
items
end end
items
end end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def by_group(items) def by_group(items)
return items unless group? if group?
items.for_group_and_descendants(group)
groups = group.self_and_descendants else
project_todos = items.where(project_id: Project.where(group: groups).select(:id)) items
group_todos = items.where(group_id: groups.select(:id)) end
Todo.from_union([project_todos, group_todos])
end end
# rubocop: enable CodeReuse/ActiveRecord
def by_state(items) def by_state(items)
case params[:state].to_s if params[:state].to_s == 'done'
when 'done'
items.done items.done
else else
items.pending items.pending
end end
end end
# rubocop: disable CodeReuse/ActiveRecord
def by_type(items) def by_type(items)
if type? if type?
items = items.where(target_type: type) items.for_type(type)
else
items
end end
items
end end
# rubocop: enable CodeReuse/ActiveRecord
end end
...@@ -386,6 +386,13 @@ class Project < ActiveRecord::Base ...@@ -386,6 +386,13 @@ class Project < ActiveRecord::Base
only_integer: true, only_integer: true,
message: 'needs to be beetween 10 minutes and 1 month' } message: 'needs to be beetween 10 minutes and 1 month' }
# Returns a project, if it is not about to be removed.
#
# id - The ID of the project to retrieve.
def self.find_without_deleted(id)
without_deleted.find_by_id(id)
end
# Paginates a collection using a `WHERE id < ?` condition. # Paginates a collection using a `WHERE id < ?` condition.
# #
# before - A project ID to use for filtering out projects with an equal or # before - A project ID to use for filtering out projects with an equal or
...@@ -450,6 +457,7 @@ class Project < ActiveRecord::Base ...@@ -450,6 +457,7 @@ class Project < ActiveRecord::Base
scope :joins_import_state, -> { joins("LEFT JOIN project_mirror_data import_state ON import_state.project_id = projects.id") } scope :joins_import_state, -> { joins("LEFT JOIN project_mirror_data import_state ON import_state.project_id = projects.id") }
scope :import_started, -> { joins_import_state.where("import_state.status = 'started' OR projects.import_status = 'started'") } scope :import_started, -> { joins_import_state.where("import_state.status = 'started' OR projects.import_status = 'started'") }
scope :for_group, -> (group) { where(group: group) }
class << self class << self
# Searches for a list of projects based on the query given in `query`. # Searches for a list of projects based on the query given in `query`.
......
...@@ -40,6 +40,11 @@ class Todo < ActiveRecord::Base ...@@ -40,6 +40,11 @@ class Todo < ActiveRecord::Base
scope :pending, -> { with_state(:pending) } scope :pending, -> { with_state(:pending) }
scope :done, -> { with_state(:done) } scope :done, -> { with_state(:done) }
scope :for_action, -> (action) { where(action: action) }
scope :for_author, -> (author) { where(author: author) }
scope :for_project, -> (project) { where(project: project) }
scope :for_group, -> (group) { where(group: group) }
scope :for_type, -> (type) { where(target_type: type) }
state_machine :state, initial: :pending do state_machine :state, initial: :pending do
event :done do event :done do
...@@ -53,6 +58,20 @@ class Todo < ActiveRecord::Base ...@@ -53,6 +58,20 @@ class Todo < ActiveRecord::Base
after_save :keep_around_commit, if: :commit_id after_save :keep_around_commit, if: :commit_id
class << self class << self
# Returns all todos for the given group and its descendants.
#
# group - A `Group` to retrieve todos for.
#
# Returns an `ActiveRecord::Relation`.
def for_group_and_descendants(group)
groups = group.self_and_descendants
from_union([
for_project(Project.for_group(groups)),
for_group(groups)
])
end
# Priority sorting isn't displayed in the dropdown, because we don't show # Priority sorting isn't displayed in the dropdown, because we don't show
# milestones, but still show something if the user has a URL with that # milestones, but still show something if the user has a URL with that
# selected. # selected.
......
...@@ -105,7 +105,6 @@ describe TodosFinder do ...@@ -105,7 +105,6 @@ describe TodosFinder do
todos = finder.new(user, { sort: 'priority' }).execute todos = finder.new(user, { sort: 'priority' }).execute
puts todos.to_sql
expect(todos).to eq([todo_3, todo_5, todo_4, todo_2, todo_1]) expect(todos).to eq([todo_3, todo_5, todo_4, todo_2, todo_1])
end end
end end
......
...@@ -4073,6 +4073,29 @@ describe Project do ...@@ -4073,6 +4073,29 @@ describe Project do
end end
end end
describe '.find_without_deleted' do
it 'returns nil if the project is about to be removed' do
project = create(:project, pending_delete: true)
expect(described_class.find_without_deleted(project.id)).to be_nil
end
it 'returns a project when it is not about to be removed' do
project = create(:project)
expect(described_class.find_without_deleted(project.id)).to eq(project)
end
end
describe '.for_group' do
it 'returns the projects for a given group' do
group = create(:group)
project = create(:project, namespace: group)
expect(described_class.for_group(group)).to eq([project])
end
end
def rugged_config def rugged_config
rugged_repo(project.repository).config rugged_repo(project.repository).config
end end
......
...@@ -173,4 +173,73 @@ describe Todo do ...@@ -173,4 +173,73 @@ describe Todo do
expect(subject).not_to be_self_assigned expect(subject).not_to be_self_assigned
end end
end end
describe '.for_action' do
it 'returns the todos for a given action' do
create(:todo, action: Todo::MENTIONED)
todo = create(:todo, action: Todo::ASSIGNED)
expect(described_class.for_action(Todo::ASSIGNED)).to eq([todo])
end
end
describe '.for_author' do
it 'returns the todos for a given author' do
user1 = create(:user)
user2 = create(:user)
todo = create(:todo, author: user1)
create(:todo, author: user2)
expect(described_class.for_author(user1)).to eq([todo])
end
end
describe '.for_project' do
it 'returns the todos for a given project' do
project1 = create(:project)
project2 = create(:project)
todo = create(:todo, project: project1)
create(:todo, project: project2)
expect(described_class.for_project(project1)).to eq([todo])
end
end
describe '.for_group' do
it 'returns the todos for a given group' do
group1 = create(:group)
group2 = create(:group)
todo = create(:todo, group: group1)
create(:todo, group: group2)
expect(described_class.for_group(group1)).to eq([todo])
end
end
describe '.for_type' do
it 'returns the todos for a given target type' do
todo = create(:todo, target: create(:issue))
create(:todo, target: create(:merge_request))
expect(described_class.for_type(Issue)).to eq([todo])
end
end
describe '.for_group_and_descendants' do
it 'returns the todos for a group and its descendants' do
parent_group = create(:group)
child_group = create(:group, parent: parent_group)
todo1 = create(:todo, group: parent_group)
todo2 = create(:todo, group: child_group)
expect(described_class.for_group_and_descendants(parent_group))
.to include(todo1, todo2)
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment