Skip to content
Snippets Groups Projects
Commit 0c6cdd07 authored by Sean McGivern's avatar Sean McGivern
Browse files

Make finders responsible for counter cache keys

parent 8deece32
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -20,6 +20,7 @@
#
class IssuableFinder
NONE = '0'.freeze
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze
 
attr_accessor :current_user, :params
 
Loading
Loading
@@ -86,6 +87,10 @@ class IssuableFinder
execute.find_by!(*params)
end
 
def state_counter_cache_key(state)
Digest::SHA1.hexdigest(state_counter_cache_key_components(state).flatten.join('-'))
end
def group
return @group if defined?(@group)
 
Loading
Loading
@@ -418,4 +423,13 @@ class IssuableFinder
def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end
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
end
Loading
Loading
@@ -22,7 +22,7 @@ class IssuesFinder < IssuableFinder
Issue
end
 
def not_restricted_by_confidentiality
def with_confidentiality_access_check
return Issue.all if user_can_see_all_confidential_issues?
return Issue.where('issues.confidential IS NOT TRUE') if user_cannot_see_confidential_issues?
 
Loading
Loading
@@ -36,7 +36,15 @@ class IssuesFinder < IssuableFinder
project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id))
end
 
private
def init_collection
with_confidentiality_access_check
end
def user_can_see_all_confidential_issues?
return @user_can_see_all_confidential_issues if defined?(@user_can_see_all_confidential_issues)
return @user_can_see_all_confidential_issues = false if current_user.blank?
return @user_can_see_all_confidential_issues = true if current_user.full_private_access?
 
Loading
Loading
@@ -46,16 +54,19 @@ class IssuesFinder < IssuableFinder
project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL
end
 
def user_cannot_see_confidential_issues?
def user_cannot_see_confidential_issues?(for_counting: false)
return false if user_can_see_all_confidential_issues?
 
current_user.blank? || params[:for_counting]
current_user.blank? || for_counting || params[:for_counting]
end
 
private
def state_counter_cache_key_components(state)
extra_components = [
user_can_see_all_confidential_issues?,
user_cannot_see_confidential_issues?(for_counting: true)
]
 
def init_collection
not_restricted_by_confidentiality
super + extra_components
end
 
def by_assignee(items)
Loading
Loading
Loading
Loading
@@ -253,7 +253,7 @@ module IssuablesHelper
 
def issuables_count_for_state(issuable_type, state, finder: nil)
finder ||= public_send("#{issuable_type}_finder")
cache_key = issuables_state_counter_cache_key(issuable_type, finder, state)
cache_key = finder.state_counter_cache_key(state)
 
@counts ||= {}
@counts[cache_key] ||= Rails.cache.fetch(cache_key, expires_in: 2.minutes) do
Loading
Loading
@@ -263,25 +263,6 @@ module IssuablesHelper
@counts[cache_key][state]
end
 
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze
private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY
def issuables_state_counter_cache_key(issuable_type, finder, state)
opts = params.with_indifferent_access
opts[:state] = state
opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
opts.delete_if { |_, value| value.blank? }
key_components = ['issuables_count', issuable_type, opts.sort]
if issuable_type == :issues
key_components << finder.user_can_see_all_confidential_issues?
key_components << finder.user_cannot_see_confidential_issues?
end
hexdigest(key_components.flatten.join('-'))
end
def issuable_templates(issuable)
@issuable_templates ||=
case issuable
Loading
Loading
Loading
Loading
@@ -295,7 +295,7 @@ describe IssuesFinder do
end
end
 
describe '#not_restricted_by_confidentiality' do
describe '#with_confidentiality_access_check' do
let(:guest) { create(:user) }
set(:authorized_user) { create(:user) }
set(:project) { create(:empty_project, namespace: authorized_user.namespace) }
Loading
Loading
@@ -306,7 +306,7 @@ describe IssuesFinder do
let(:params) { {} }
 
context 'for an anonymous user' do
subject { described_class.new(nil, params).not_restricted_by_confidentiality }
subject { described_class.new(nil, params).with_confidentiality_access_check }
 
it 'returns only public issues' do
expect(subject).to include(public_issue)
Loading
Loading
@@ -315,7 +315,7 @@ describe IssuesFinder do
end
 
context 'for a user without project membership' do
subject { described_class.new(user, params).not_restricted_by_confidentiality }
subject { described_class.new(user, params).with_confidentiality_access_check }
 
it 'returns only public issues' do
expect(subject).to include(public_issue)
Loading
Loading
@@ -324,7 +324,7 @@ describe IssuesFinder do
end
 
context 'for a guest user' do
subject { described_class.new(guest, params).not_restricted_by_confidentiality }
subject { described_class.new(guest, params).with_confidentiality_access_check }
 
before do
project.add_guest(guest)
Loading
Loading
@@ -337,7 +337,7 @@ describe IssuesFinder do
end
 
context 'for a project member with access to view confidential issues' do
subject { described_class.new(authorized_user, params).not_restricted_by_confidentiality }
subject { described_class.new(authorized_user, params).with_confidentiality_access_check }
 
it 'returns all issues' do
expect(subject).to include(public_issue, confidential_issue)
Loading
Loading
@@ -349,7 +349,7 @@ describe IssuesFinder do
let(:params) { { project_id: project.id } }
 
context 'for an anonymous user' do
subject { described_class.new(nil, params).not_restricted_by_confidentiality }
subject { described_class.new(nil, params).with_confidentiality_access_check }
 
it 'returns only public issues' do
expect(subject).to include(public_issue)
Loading
Loading
@@ -364,7 +364,7 @@ describe IssuesFinder do
end
 
context 'for a user without project membership' do
subject { described_class.new(user, params).not_restricted_by_confidentiality }
subject { described_class.new(user, params).with_confidentiality_access_check }
 
it 'returns only public issues' do
expect(subject).to include(public_issue)
Loading
Loading
@@ -379,7 +379,7 @@ describe IssuesFinder do
end
 
context 'for a guest user' do
subject { described_class.new(guest, params).not_restricted_by_confidentiality }
subject { described_class.new(guest, params).with_confidentiality_access_check }
 
before do
project.add_guest(guest)
Loading
Loading
@@ -398,7 +398,7 @@ describe IssuesFinder do
end
 
context 'for a project member with access to view confidential issues' do
subject { described_class.new(authorized_user, params).not_restricted_by_confidentiality }
subject { described_class.new(authorized_user, params).with_confidentiality_access_check }
 
it 'returns all issues' do
expect(subject).to include(public_issue, confidential_issue)
Loading
Loading
Loading
Loading
@@ -77,59 +77,57 @@ describe IssuablesHelper do
}.with_indifferent_access
end
 
let(:finder) { double(:finder, user_cannot_see_confidential_issues?: true, user_can_see_all_confidential_issues?: false) }
let(:issues_finder) { IssuesFinder.new(nil, params) }
let(:merge_requests_finder) { MergeRequestsFinder.new(nil, params) }
 
before do
allow(helper).to receive(:issues_finder).and_return(finder)
allow(helper).to receive(:merge_requests_finder).and_return(finder)
allow(helper).to receive(:issues_finder).and_return(issues_finder)
allow(helper).to receive(:merge_requests_finder).and_return(merge_requests_finder)
end
 
it 'returns the cached value when called for the same issuable type & with the same params' do
expect(helper).to receive(:params).twice.and_return(params)
expect(finder).to receive(:count_by_state).and_return(opened: 42)
expect(issues_finder).to receive(:count_by_state).and_return(opened: 42)
 
expect(helper.issuables_state_counter_text(:issues, :opened))
.to eq('<span>Open</span> <span class="badge">42</span>')
 
expect(finder).not_to receive(:count_by_state)
expect(issues_finder).not_to receive(:count_by_state)
 
expect(helper.issuables_state_counter_text(:issues, :opened))
.to eq('<span>Open</span> <span class="badge">42</span>')
end
 
it 'takes confidential status into account when searching for issues' do
allow(helper).to receive(:params).and_return(params)
expect(finder).to receive(:count_by_state).and_return(opened: 42)
expect(issues_finder).to receive(:count_by_state).and_return(opened: 42)
 
expect(helper.issuables_state_counter_text(:issues, :opened))
.to include('42')
 
expect(finder).to receive(:user_cannot_see_confidential_issues?).and_return(false)
expect(finder).to receive(:count_by_state).and_return(opened: 40)
expect(issues_finder).to receive(:user_cannot_see_confidential_issues?).twice.and_return(false)
expect(issues_finder).to receive(:count_by_state).and_return(opened: 40)
 
expect(helper.issuables_state_counter_text(:issues, :opened))
.to include('40')
 
expect(finder).to receive(:user_can_see_all_confidential_issues?).and_return(true)
expect(finder).to receive(:count_by_state).and_return(opened: 45)
expect(issues_finder).to receive(:user_can_see_all_confidential_issues?).and_return(true)
expect(issues_finder).to receive(:count_by_state).and_return(opened: 45)
 
expect(helper.issuables_state_counter_text(:issues, :opened))
.to include('45')
end
 
it 'does not take confidential status into account when searching for merge requests' do
allow(helper).to receive(:params).and_return(params)
expect(finder).to receive(:count_by_state).and_return(opened: 42)
expect(finder).not_to receive(:user_cannot_see_confidential_issues?)
expect(finder).not_to receive(:user_can_see_all_confidential_issues?)
expect(merge_requests_finder).to receive(:count_by_state).and_return(opened: 42)
expect(merge_requests_finder).not_to receive(:user_cannot_see_confidential_issues?)
expect(merge_requests_finder).not_to receive(:user_can_see_all_confidential_issues?)
 
expect(helper.issuables_state_counter_text(:merge_requests, :opened))
.to include('42')
end
 
it 'does not take some keys into account in the cache key' do
expect(finder).to receive(:count_by_state).and_return(opened: 42)
expect(helper).to receive(:params).and_return({
expect(issues_finder).to receive(:count_by_state).and_return(opened: 42)
expect(issues_finder).to receive(:params).and_return({
author_id: '11',
state: 'foo',
sort: 'foo',
Loading
Loading
@@ -140,8 +138,8 @@ describe IssuablesHelper do
expect(helper.issuables_state_counter_text(:issues, :opened))
.to eq('<span>Open</span> <span class="badge">42</span>')
 
expect(finder).not_to receive(:count_by_state)
expect(helper).to receive(:params).and_return({
expect(issues_finder).not_to receive(:count_by_state)
expect(issues_finder).to receive(:params).and_return({
author_id: '11',
state: 'bar',
sort: 'bar',
Loading
Loading
@@ -154,14 +152,14 @@ describe IssuablesHelper do
end
 
it 'does not take params order into account in the cache key' do
expect(helper).to receive(:params).and_return('author_id' => '11', 'state' => 'opened')
expect(finder).to receive(:count_by_state).and_return(opened: 42)
expect(issues_finder).to receive(:params).and_return('author_id' => '11', 'state' => 'opened')
expect(issues_finder).to receive(:count_by_state).and_return(opened: 42)
 
expect(helper.issuables_state_counter_text(:issues, :opened))
.to eq('<span>Open</span> <span class="badge">42</span>')
 
expect(helper).to receive(:params).and_return('state' => 'opened', 'author_id' => '11')
expect(finder).not_to receive(:count_by_state)
expect(issues_finder).to receive(:params).and_return('state' => 'opened', 'author_id' => '11')
expect(issues_finder).not_to receive(:count_by_state)
 
expect(helper.issuables_state_counter_text(:issues, :opened))
.to eq('<span>Open</span> <span class="badge">42</span>')
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment