diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index e8605f3d5b3..7bc2117f61e 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -20,6 +20,7 @@ # class IssuableFinder NONE = '0'.freeze + IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze attr_accessor :current_user, :params @@ -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) @@ -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 diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index b213a7aebfd..d20f4475a03 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -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? @@ -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? @@ -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) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index d99a9bab12f..5385ada6dc4 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -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 @@ -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 diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index dfa15e859a4..4a52f0d5c58 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -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) } @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 7dfda388de4..d2e918ef014 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -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('Open 42') - 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('Open 42') 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', @@ -140,8 +138,8 @@ describe IssuablesHelper do expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('Open 42') - 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', @@ -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('Open 42') - 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('Open 42')