Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 29 additions & 25 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -516,36 +516,40 @@ def category_badge(category, opts = nil)
CategoryBadge.html_for(category, opts).html_safe
end

SERVER_PLUGIN_OUTLET_PLUGINS_PREFIXES = [Rails.root.join("plugins/").to_s]
private_constant :SERVER_PLUGIN_OUTLET_PLUGINS_PREFIXES

if Rails.env.test?
SERVER_PLUGIN_OUTLET_PLUGINS_PREFIXES << Rails.root.join("spec/fixtures/plugins/").to_s
end

SERVER_PLUGIN_OUTLET_CONNECTOR_TEMPLATES =
SERVER_PLUGIN_OUTLET_PLUGINS_PREFIXES.each_with_object({}) do |plugins_prefix, connectors|
Dir
.glob("#{plugins_prefix}*/app/views/connectors/**/*.html.erb")
.each do |template_path|
template_path =~ Regexp.new("/connectors/(.*)/.*\.html\.erb$")
outlet_name = Regexp.last_match(1)
connectors[outlet_name] ||= []
connectors[outlet_name] << template_path.sub(plugins_prefix, "").delete_suffix(
".html.erb",
)
end
end
private_constant :SERVER_PLUGIN_OUTLET_CONNECTOR_TEMPLATES
def self.all_connectors
@all_connectors = Dir.glob("plugins/*/app/views/connectors/**/*.html.erb")
end

PLUGIN_OUTLET_TEMPLATE_CACHE = Concurrent::Map.new

def server_plugin_outlet(name, locals: {})
return "" if !GlobalSetting.load_plugins?
return "" if !SERVER_PLUGIN_OUTLET_CONNECTOR_TEMPLATES.key?(name)

lookup_context.append_view_paths(SERVER_PLUGIN_OUTLET_PLUGINS_PREFIXES)
matcher = Regexp.new("/connectors/#{name}/.*\.html\.erb$")
erbs = ApplicationHelper.all_connectors.select { |c| c =~ matcher }
return "" if erbs.blank?

erbs
.map do |erb|
cache_key = [erb, locals.keys.sort]

template =
PLUGIN_OUTLET_TEMPLATE_CACHE.compute_if_absent(cache_key) do
source = File.read(erb)
handler = ActionView::Template.handler_for_extension("erb")

ActionView::Template.new(
source,
"discourse_plugin_outlet__#{name}",
handler,
locals: locals.keys,
format: :html,
virtual_path: erb,
)
end

SERVER_PLUGIN_OUTLET_CONNECTOR_TEMPLATES[name]
.map { |template| render template:, locals: }
render template: template, locals: locals
end
.join
.html_safe
end
Expand Down
5 changes: 5 additions & 0 deletions config/initializers/development.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

if Rails.env.development?
Rails.application.reloader.to_prepare { ApplicationHelper::PLUGIN_OUTLET_TEMPLATE_CACHE.clear }
end

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

51 changes: 29 additions & 22 deletions spec/requests/topics_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,35 +40,42 @@
describe "topic_header plugin outlet" do
fab!(:another_topic) { Fabricate(:topic, title: "Another topic by me") }

before { global_setting(:load_plugins?, true) }

it "renders the connector templates from multiple plugins" do
get "/t/#{topic.slug}/#{topic.id}"
let(:tmp_dir) { Dir.mktmpdir }
let(:template_dir) do
path = File.join(tmp_dir, "connectors", "topic_header")
FileUtils.mkdir_p(path)
path
end
let(:template_file) do
file = Tempfile.new(%w[test_template .html.erb], template_dir)
file.write("Topic title from outlet: <%= @topic_view.topic.title %>")
file.close
file
end

expect(response.status).to eq(200)
expect(response.body).to include("Fixture from my_plugin template 1: #{topic.title}")
expect(response.body).to include("Fixture from my_plugin template 2: #{topic.title}")
expect(response.body).to include("Fixture from my_plugin_2 template 1: #{topic.title}")
expect(response.body).to include("Fixture from my_plugin_2 template 2: #{topic.title}")
expect(response.body).not_to include("Fixture from my_plugin_3 template 1: #{topic.title}")
before do
global_setting(:load_plugins?, true)
ApplicationHelper.stubs(:all_connectors).returns([template_file.path])
end

get "/t/#{another_topic.slug}/#{another_topic.id}"
after { FileUtils.remove_entry(tmp_dir) if tmp_dir }

it "doesn't leak state between requests" do
get "/t/#{topic.slug}/#{topic.id}"
expect(response.status).to eq(200)
expect(response.body).to include("Fixture from my_plugin template 1: #{another_topic.title}")
expect(response.body).to include("Fixture from my_plugin template 2: #{another_topic.title}")
expect(response.body).to include("Topic title from outlet: #{topic.title}")

expect(response.body).to include(
"Fixture from my_plugin_2 template 1: #{another_topic.title}",
)
# Verify that the template cache is working correctly
expect(ApplicationHelper::PLUGIN_OUTLET_TEMPLATE_CACHE.size).to eq(1)
cache_keys = ApplicationHelper::PLUGIN_OUTLET_TEMPLATE_CACHE.keys
expect(cache_keys.first.first).to eq(template_file.path)

expect(response.body).to include(
"Fixture from my_plugin_2 template 2: #{another_topic.title}",
)
get "/t/#{another_topic.slug}/#{another_topic.id}"
expect(response.status).to eq(200)
expect(response.body).to include("Topic title from outlet: #{another_topic.title}")

expect(response.body).not_to include(
"Fixture from my_plugin_3 template 1: #{another_topic.title}",
)
# Verify that the cache is reused and not duplicated
expect(ApplicationHelper::PLUGIN_OUTLET_TEMPLATE_CACHE.size).to eq(1)
end
end

Expand Down
Loading