diff --git a/app/controllers/concerns/liquid_enabled_resource.rb b/app/controllers/concerns/liquid_enabled_resource.rb index 1dac5829e..aa0661e31 100644 --- a/app/controllers/concerns/liquid_enabled_resource.rb +++ b/app/controllers/concerns/liquid_enabled_resource.rb @@ -35,6 +35,6 @@ def project_assigns project = Project.find(params[:project_id]) authorize! :use, project - LiquidAssignsService.new(project: project, text: params[:text]).assigns + LiquidCachedAssigns.new(project: project) end end diff --git a/app/services/liquid_assigns_service.rb b/app/services/liquid_assigns_service.rb deleted file mode 100644 index b781c77b6..000000000 --- a/app/services/liquid_assigns_service.rb +++ /dev/null @@ -1,61 +0,0 @@ -class LiquidAssignsService - AVAILABLE_PROJECT_ASSIGNS = %w{ evidences issues nodes notes tags }.freeze - - attr_accessor :project, :text - - def initialize(project:, text: nil) - @project = project - @text = text - end - - def assigns - result = project_assigns - result.merge!(assigns_pro) if defined?(Dradis::Pro) - result - end - - private - - def assigns_pro - end - - # This method uses Liquid::VariableLookup to find all liquid variables from - # a given text. We use the list to know which project assign we need. - def assigns_from_content - return AVAILABLE_PROJECT_ASSIGNS if text.nil? - - variable_lookup = Liquid::VariableLookup.parse(text) - return (variable_lookup.lookups & AVAILABLE_PROJECT_ASSIGNS) - end - - def cached_drops(records, record_type) - return [] if records.empty? - - cache_key = "liquid-project-#{project.id}-#{record_type.pluralize}:#{records.maximum(:updated_at).to_i}-#{records.count}" - drop_class = "#{record_type.camelize}Drop".constantize - - Rails.cache.fetch(cache_key) do - records.map { |record| drop_class.new(record) } - end - end - - def project_assigns - project_assigns = { 'project' => ProjectDrop.new(project) } - - assigns_from_content.each do |record_type| - records = - case record_type - when 'evidences' - project.evidence - when 'nodes' - project.nodes.user_nodes - else - project.send(record_type.to_sym) - end - - project_assigns.merge!(record_type => cached_drops(records, record_type.singularize)) - end - - project_assigns - end -end diff --git a/app/services/liquid_cached_assigns.rb b/app/services/liquid_cached_assigns.rb new file mode 100644 index 000000000..5f1e82562 --- /dev/null +++ b/app/services/liquid_cached_assigns.rb @@ -0,0 +1,70 @@ +class LiquidCachedAssigns < Hash + AVAILABLE_PROJECT_ASSIGNS = %w{ evidences issues nodes notes project tags }.freeze + + attr_accessor :assigns, :project + + def initialize(project:) + @project = project + + @assigns = { 'project' => ProjectDrop.new(project) } + @assigns.merge!(assigns_pro) + end + + def [](record_type) + assigns[record_type] ||= cached_drops(record_type) + end + + # SEE: https://github.com/Shopify/liquid/blob/77bc56/lib/liquid/context.rb#L211 + # Liquid is checking if the variable is present in the assigns hash by + # calling the `key?` method. Since we're lazily loading the keys, the variable + # may not yet be present in the assigns hash. + def key?(key) + AVAILABLE_PROJECT_ASSIGNS.include?(key.to_s) || assigns.key?(key) + end + + def merge(hash) + lca = LiquidCachedAssigns.new(project: project) + lca.assigns = @assigns.merge(hash) + lca + end + + def merge!(hash) + @assigns.merge!(hash) + self + end + + private + + def assigns_pro + {} + end + + def cached_drops(record_type) + records = project_records(record_type) + + return [] if records.empty? + + cache_key = ActiveSupport::Cache.expand_cache_key([project.id, records], 'liquid') + drop_class = "#{record_type.singularize.camelize}Drop".constantize + + Rails.cache.fetch(cache_key) do + records.map { |record| drop_class.new(record) } + end + end + + def project_records(record_type) + return [] unless AVAILABLE_PROJECT_ASSIGNS.include?(record_type) + + case record_type + when 'evidences' + project.evidence + when 'nodes' + project.nodes.user_nodes + when 'notes' + # FIXME - ISSUE/NOTE INHERITANCE + project.notes.where.not(node_id: project.issue_library.id) + else + project.send(record_type.to_sym) + end + end +end diff --git a/spec/services/liquid_assigns_service_spec.rb b/spec/services/liquid_assigns_service_spec.rb deleted file mode 100644 index 5e1fd0796..000000000 --- a/spec/services/liquid_assigns_service_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'rails_helper' - -RSpec.describe LiquidAssignsService do - let!(:project) { create(:project) } - - before do - node = create(:node, project: project) - issue = create(:issue, node: project.issue_library) - create(:evidence, issue: issue, node: node) - create(:note, node: node) - create(:tag) - end - - describe '#project_assigns' do - context 'with the :text argument' do - LiquidAssignsService::AVAILABLE_PROJECT_ASSIGNS.each do |assign| - it "adds #{assign} to the project_assigns if present in the text" do - text = "#[Description]#\n {% for #{assign.singularize} in #{assign} %}{% endfor %}\n" - liquid_assigns = described_class.new(project: project, text: text).assigns - - expect(liquid_assigns.keys).to include(assign) - end - end - end - - context 'without the :text argument' do - let(:liquid_assigns) { described_class.new(project: project).assigns } - - it 'builds a hash of liquid assigns' do - expect(liquid_assigns['project'].name).to eq(project.name) - expect(liquid_assigns['issues'].map(&:title)).to eq(project.issues.map(&:title)) - expect(liquid_assigns['evidences'].map(&:title)).to eq(project.evidence.map(&:title)) - expect(liquid_assigns['nodes'].map(&:label)).to eq(project.nodes.user_nodes.map(&:label)) - expect(liquid_assigns['notes'].map(&:title)).to eq(project.notes.map(&:title)) - expect(liquid_assigns['tags'].map(&:display_name)).to eq(project.tags.map(&:display_name)) - end - end - end - - context 'with pro records', skip: !defined?(Dradis::Pro) do - let(:liquid_assigns) { described_class.new(project: project).assigns } - - let!(:project) { create(:project, :with_team) } - - before do - report_content = project.content_library - report_content.properties = { 'dradis.project' => project.name } - report_content.save - - create(:content_block, project: project) - end - - it 'builds a hash with Dradis::Pro assigns' do - expect(liquid_assigns['document_properties'].available_properties).to eq({ 'dradis.project' => project.name }) - expect(liquid_assigns['team'].name).to eq(project.team.name) - expect(liquid_assigns['content_blocks'].map(&:content)).to eq(project.content_blocks.map(&:content)) - end - end -end diff --git a/spec/services/liquid_cached_assigns_spec.rb b/spec/services/liquid_cached_assigns_spec.rb new file mode 100644 index 000000000..34889a466 --- /dev/null +++ b/spec/services/liquid_cached_assigns_spec.rb @@ -0,0 +1,63 @@ +require 'rails_helper' + +RSpec.describe LiquidCachedAssigns do + let!(:project) { create(:project) } + let(:liquid_assigns) { described_class.new(project: project) } + + before do + node = create(:node, project: project) + issue = create(:issue, node: project.issue_library) + create(:evidence, issue: issue, node: node) + create(:note, node: node) + create(:tag) + end + + context 'fetching an assign from an available collection' do + it 'lazily loads the assigns' do + expect(liquid_assigns.assigns.keys).to_not include( + %w{issues evidences nodes notes tags} + ) + end + + it 'builds a hash of liquid assigns' do + issues = project.issues.map(&:title) + + expect(liquid_assigns['project'].name).to eq(project.name) + expect(liquid_assigns['issues'].map(&:title)).to eq(issues) + expect(liquid_assigns['evidences'].map(&:title)).to eq(project.evidence.map(&:title)) + expect(liquid_assigns['nodes'].map(&:label)).to eq(project.nodes.user_nodes.map(&:label)) + expect(liquid_assigns['notes'].map(&:title)).to eq(project.notes.map(&:title) - issues) + expect(liquid_assigns['tags'].map(&:display_name)).to eq(project.tags.map(&:display_name)) + end + end + + context 'fetching an assign from a unavailable collection' do + it 'returns an empty array' do + expect(liquid_assigns['fake']).to be_empty + end + end + + context 'with pro records', skip: !defined?(Dradis::Pro) do + let!(:project) { create(:project, :with_team) } + + before do + report_content = project.content_library + report_content.properties = { 'dradis.project' => project.name } + report_content.save + + create(:content_block, project: project) + end + + context 'fetching an assign from an available collection' do + it 'lazily loads the assigns' do + expect(liquid_assigns.assigns.keys).to_not include('content_blocks') + end + + it 'builds a hash with Dradis::Pro assigns' do + expect(liquid_assigns['document_properties'].available_properties).to eq({ 'dradis.project' => project.name }) + expect(liquid_assigns['team'].name).to eq(project.team.name) + expect(liquid_assigns['content_blocks'].map(&:content)).to eq(project.content_blocks.map(&:content)) + end + end + end +end