Skip to content

Commit da1c99e

Browse files
FEATURE: Add script to post report results in a topic regularly (#328)
This script is similar to the existing one that schedules report results to to be sent to a PM on a regular basis, but instead takes a topic ID and posts to that. This way people can have report results sent to a public topic regularly too and not have to deal with PM recipients and so on.
1 parent d306466 commit da1c99e

8 files changed

+410
-54
lines changed

config/locales/client.en.yml

+12
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,15 @@ en:
118118
label: Skip sending PM if there are no results
119119
attach_csv:
120120
label: Attach the CSV file to the PM
121+
recurring_data_explorer_result_topic:
122+
fields:
123+
topic_id:
124+
label: The topic to post query results in
125+
query_id:
126+
label: Data Explorer Query
127+
query_params:
128+
label: Data Explorer Query parameters
129+
skip_empty:
130+
label: Skip posting if there are no results
131+
attach_csv:
132+
label: Attach the CSV file to the post

config/locales/server.en.yml

+31-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,34 @@ en:
55
scriptables:
66
recurring_data_explorer_result_pm:
77
title: Schedule a PM with Data Explorer results
8-
description: Get scheduled reports sent to your messages each month
8+
description: Get scheduled reports sent to your messages
9+
recurring_data_explorer_result_topic:
10+
title: Schedule a post in a topic with Data Explorer results
11+
description: Get scheduled reports posted to a specific topic
12+
data_explorer:
13+
report_generator:
14+
private_message:
15+
title: "Scheduled report for %{query_name}"
16+
body: |
17+
Hi %{recipient_name}, your Data Explorer report is ready.
18+
19+
Query name:
20+
%{query_name}
21+
22+
Here are the results:
23+
%{table}
24+
25+
<a href='%{base_url}/admin/plugins/explorer?id=%{query_id}'>View query in Data Explorer</a>
26+
27+
Report created at %{created_at} (%{timezone})
28+
post:
29+
body: |
30+
### Scheduled report for %{query_name}
31+
32+
Here are the results:
33+
%{table}
34+
35+
<a href='%{base_url}/admin/plugins/explorer?id=%{query_id}'>View query in Data Explorer</a>
36+
37+
Report created at %{created_at} (%{timezone})
38+
upload_appendix: "Appendix: [%{filename}|attachment](%{short_url})"

lib/discourse_data_explorer/report_generator.rb

+72-17
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@ def self.generate(query_id, query_params, recipients, opts = {})
1818
build_report_pms(query, table, recipients, attach_csv: opts[:attach_csv], result:)
1919
end
2020

21+
def self.generate_post(query_id, query_params, opts = {})
22+
query = DiscourseDataExplorer::Query.find(query_id)
23+
return {} if !query
24+
25+
params = params_to_hash(query_params)
26+
27+
result = DataExplorer.run_query(query, params)
28+
query.update!(last_run_at: Time.now)
29+
30+
return {} if opts[:skip_empty] && result[:pg_result].values.empty?
31+
table = ResultToMarkdown.convert(result[:pg_result])
32+
33+
build_report_post(query, table, attach_csv: opts[:attach_csv], result:)
34+
end
35+
2136
private
2237

2338
def self.params_to_hash(query_params)
@@ -42,37 +57,77 @@ def self.params_to_hash(query_params)
4257

4358
def self.build_report_pms(query, table = "", targets = [], attach_csv: false, result: nil)
4459
pms = []
45-
upload =
46-
if attach_csv
47-
tmp_filename =
48-
"#{query.slug}@#{Slug.for(Discourse.current_hostname, "discourse")}-#{Date.today}.dcqresult.csv"
49-
tmp = Tempfile.new(tmp_filename)
50-
tmp.write(ResultFormatConverter.convert(:csv, result))
51-
tmp.rewind
52-
UploadCreator.new(tmp, tmp_filename, type: "csv_export").create_for(
53-
Discourse.system_user.id,
54-
)
55-
end
60+
upload = create_csv_upload(query, result) if attach_csv
5661

5762
targets.each do |target|
5863
name = target[0]
5964
pm_type = "target_#{target[1]}s"
6065

6166
pm = {}
62-
pm["title"] = "Scheduled Report for #{query.name}"
67+
pm["title"] = I18n.t(
68+
"data_explorer.report_generator.private_message.title",
69+
query_name: query.name,
70+
)
6371
pm[pm_type] = Array(name)
64-
pm["raw"] = "Hi #{name}, your data explorer report is ready.\n\n" +
65-
"Query Name:\n#{query.name}\n\nHere are the results:\n#{table}\n\n" +
66-
"<a href='#{Discourse.base_url}/admin/plugins/explorer?id=#{query.id}'>View query in Data Explorer</a>\n\n" +
67-
"Report created at #{Time.zone.now.strftime("%Y-%m-%d at %H:%M:%S")} (#{Time.zone.name})"
72+
pm["raw"] = I18n.t(
73+
"data_explorer.report_generator.private_message.body",
74+
recipient_name: name,
75+
query_name: query.name,
76+
table: table,
77+
base_url: Discourse.base_url,
78+
query_id: query.id,
79+
created_at: Time.zone.now.strftime("%Y-%m-%d at %H:%M:%S"),
80+
timezone: Time.zone.name,
81+
)
6882
if upload
69-
pm["raw"] << "\n\nAppendix: [#{upload.original_filename}|attachment](#{upload.short_url})"
83+
pm["raw"] << "\n\n" +
84+
I18n.t(
85+
"data_explorer.report_generator.upload_appendix",
86+
filename: upload.original_filename,
87+
short_url: upload.short_url,
88+
)
7089
end
7190
pms << pm
7291
end
7392
pms
7493
end
7594

95+
def self.build_report_post(query, table = "", attach_csv: false, result: nil)
96+
upload = create_csv_upload(query, result) if attach_csv
97+
98+
post = {}
99+
post["raw"] = I18n.t(
100+
"data_explorer.report_generator.post.body",
101+
recipient_name: name,
102+
query_name: query.name,
103+
table: table,
104+
base_url: Discourse.base_url,
105+
query_id: query.id,
106+
created_at: Time.zone.now.strftime("%Y-%m-%d at %H:%M:%S"),
107+
timezone: Time.zone.name,
108+
)
109+
110+
if upload
111+
post["raw"] << "\n\n" +
112+
I18n.t(
113+
"data_explorer.report_generator.upload_appendix",
114+
filename: upload.original_filename,
115+
short_url: upload.short_url,
116+
)
117+
end
118+
119+
post
120+
end
121+
122+
def self.create_csv_upload(query, result)
123+
tmp_filename =
124+
"#{query.slug}@#{Slug.for(Discourse.current_hostname, "discourse")}-#{Date.today}.dcqresult.csv"
125+
tmp = Tempfile.new(tmp_filename)
126+
tmp.write(ResultFormatConverter.convert(:csv, result))
127+
tmp.rewind
128+
UploadCreator.new(tmp, tmp_filename, type: "csv_export").create_for(Discourse.system_user.id)
129+
end
130+
76131
def self.filter_recipients_by_query_access(recipients, query)
77132
users = User.where(username: recipients)
78133
groups = Group.where(name: recipients)

plugin.rb

+54
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,60 @@ module ::DiscourseDataExplorer
115115
end
116116
end
117117
end
118+
119+
add_automation_scriptable("recurring_data_explorer_result_topic") do
120+
queries =
121+
DiscourseDataExplorer::Query
122+
.where(hidden: false)
123+
.map { |q| { id: q.id, translated_name: q.name } }
124+
field :topic_id, component: :text, required: true
125+
field :query_id, component: :choices, required: true, extra: { content: queries }
126+
field :query_params, component: :"key-value", accepts_placeholders: true
127+
field :skip_empty, component: :boolean
128+
field :attach_csv, component: :boolean
129+
130+
version 1
131+
triggerables [:recurring]
132+
133+
script do |_, fields, automation|
134+
topic_id = fields.dig("topic_id", "value")
135+
query_id = fields.dig("query_id", "value")
136+
query_params = fields.dig("query_params", "value") || {}
137+
skip_empty = fields.dig("skip_empty", "value") || false
138+
attach_csv = fields.dig("attach_csv", "value") || false
139+
140+
unless SiteSetting.data_explorer_enabled
141+
Rails.logger.warn "#{DiscourseDataExplorer::PLUGIN_NAME} - plugin must be enabled to run automation #{automation.id}"
142+
next
143+
end
144+
145+
topic = Topic.find_by(id: topic_id)
146+
if topic.blank?
147+
Rails.logger.warn "#{DiscourseDataExplorer::PLUGIN_NAME} - couldn't find topic ID (#{topic_id}) for automation #{automation.id}"
148+
next
149+
end
150+
151+
begin
152+
post =
153+
DiscourseDataExplorer::ReportGenerator.generate_post(
154+
query_id,
155+
query_params,
156+
{ skip_empty:, attach_csv: },
157+
)
158+
159+
next if post.empty?
160+
161+
PostCreator.create!(
162+
Discourse.system_user,
163+
topic_id: topic.id,
164+
raw: post["raw"],
165+
skip_validations: true,
166+
)
167+
rescue ActiveRecord::RecordNotSaved => e
168+
Rails.logger.warn "#{DiscourseDataExplorer::PLUGIN_NAME} - couldn't reply to topic ID #{topic_id} for automation #{automation.id}: #{e.message}"
169+
end
170+
end
171+
end
118172
end
119173
end
120174
end

spec/automation/recurring_data_explorer_result_pm_spec.rb

+18-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
require "rails_helper"
44

5-
describe "RecurringDataExplorerResultPm" do
5+
describe "RecurringDataExplorerResultPM" do
66
fab!(:admin)
77

88
fab!(:user)
@@ -16,7 +16,7 @@
1616
fab!(:automation) do
1717
Fabricate(:automation, script: "recurring_data_explorer_result_pm", trigger: "recurring")
1818
end
19-
fab!(:query)
19+
fab!(:query) { Fabricate(:query, sql: "SELECT 'testabcd' AS data") }
2020
fab!(:query_group) { Fabricate(:query_group, query: query, group: group) }
2121
fab!(:query_group) { Fabricate(:query_group, query: query, group: another_group) }
2222

@@ -49,7 +49,8 @@
4949
freeze_time 1.day.from_now do
5050
expect { Jobs::DiscourseAutomation::Tracker.new.execute }.to change { Topic.count }.by(3)
5151

52-
title = "Scheduled Report for #{query.name}"
52+
title =
53+
I18n.t("data_explorer.report_generator.private_message.title", query_name: query.name)
5354
expect(Topic.last(3).pluck(:title)).to eq([title, title, title])
5455
end
5556
end
@@ -77,15 +78,27 @@
7778
end
7879

7980
it "has appropriate content from the report generator" do
81+
freeze_time
82+
8083
automation.update(last_updated_by_id: admin.id)
8184
automation.trigger!
8285

83-
expect(Post.last.raw).to include(
84-
"Hi #{another_group.name}, your data explorer report is ready.\n\nQuery Name:\n#{query.name}",
86+
expect(Post.last.raw).to eq(
87+
I18n.t(
88+
"data_explorer.report_generator.private_message.body",
89+
recipient_name: another_group.name,
90+
query_name: query.name,
91+
table: "| data |\n| :----- |\n| testabcd |\n",
92+
base_url: Discourse.base_url,
93+
query_id: query.id,
94+
created_at: Time.zone.now.strftime("%Y-%m-%d at %H:%M:%S"),
95+
timezone: Time.zone.name,
96+
).chomp,
8597
)
8698
end
8799

88100
it "does not send the PM if skip_empty" do
101+
query.update!(sql: "SELECT NULL LIMIT 0")
89102
automation.upsert_field!("skip_empty", "boolean", { value: true })
90103

91104
automation.update(last_updated_by_id: admin.id)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# frozen_string_literal: true
2+
3+
require "rails_helper"
4+
5+
describe "RecurringDataExplorerResultTopic" do
6+
fab!(:admin)
7+
8+
fab!(:user)
9+
fab!(:another_user) { Fabricate(:user) }
10+
fab!(:group_user) { Fabricate(:user) }
11+
fab!(:not_allowed_user) { Fabricate(:user) }
12+
fab!(:topic)
13+
14+
fab!(:group) { Fabricate(:group, users: [user, another_user]) }
15+
fab!(:another_group) { Fabricate(:group, users: [group_user]) }
16+
17+
fab!(:automation) do
18+
Fabricate(:automation, script: "recurring_data_explorer_result_topic", trigger: "recurring")
19+
end
20+
fab!(:query) { Fabricate(:query, sql: "SELECT 'testabcd' AS data") }
21+
fab!(:query_group) { Fabricate(:query_group, query: query, group: group) }
22+
fab!(:query_group) { Fabricate(:query_group, query: query, group: another_group) }
23+
24+
before do
25+
SiteSetting.data_explorer_enabled = true
26+
SiteSetting.discourse_automation_enabled = true
27+
28+
automation.upsert_field!("query_id", "choices", { value: query.id })
29+
automation.upsert_field!("topic_id", "text", { value: topic.id })
30+
automation.upsert_field!(
31+
"query_params",
32+
"key-value",
33+
{ value: [%w[from_days_ago 0], %w[duration_days 15]] },
34+
)
35+
automation.upsert_field!(
36+
"recurrence",
37+
"period",
38+
{ value: { interval: 1, frequency: "day" } },
39+
target: "trigger",
40+
)
41+
automation.upsert_field!("start_date", "date_time", { value: 2.minutes.ago }, target: "trigger")
42+
end
43+
44+
context "when using recurring trigger" do
45+
it "sends the post at recurring date_date" do
46+
freeze_time 1.day.from_now do
47+
expect { Jobs::DiscourseAutomation::Tracker.new.execute }.to change {
48+
topic.reload.posts.count
49+
}.by(1)
50+
51+
expect(topic.posts.last.raw).to eq(
52+
I18n.t(
53+
"data_explorer.report_generator.post.body",
54+
query_name: query.name,
55+
table: "| data |\n| :----- |\n| testabcd |\n",
56+
base_url: Discourse.base_url,
57+
query_id: query.id,
58+
created_at: Time.zone.now.strftime("%Y-%m-%d at %H:%M:%S"),
59+
timezone: Time.zone.name,
60+
).chomp,
61+
)
62+
end
63+
end
64+
65+
it "has appropriate content from the report generator" do
66+
freeze_time
67+
automation.update(last_updated_by_id: admin.id)
68+
automation.trigger!
69+
70+
expect(topic.posts.last.raw).to eq(
71+
I18n.t(
72+
"data_explorer.report_generator.post.body",
73+
query_name: query.name,
74+
table: "| data |\n| :----- |\n| testabcd |\n",
75+
base_url: Discourse.base_url,
76+
query_id: query.id,
77+
created_at: Time.zone.now.strftime("%Y-%m-%d at %H:%M:%S"),
78+
timezone: Time.zone.name,
79+
).chomp,
80+
)
81+
end
82+
83+
it "does not create the post if skip_empty" do
84+
query.update!(sql: "SELECT NULL LIMIT 0")
85+
automation.upsert_field!("skip_empty", "boolean", { value: true })
86+
87+
automation.update(last_updated_by_id: admin.id)
88+
89+
expect { automation.trigger! }.to_not change { Post.count }
90+
end
91+
end
92+
end

spec/fabricators/query_fabricator.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Fabricator(:query, from: DiscourseDataExplorer::Query) do
44
name { sequence(:name) { |i| "cat#{i}" } }
55
description { sequence(:desc) { |i| "description #{i}" } }
6-
sql { sequence(:sql) { |i| "SELECT * FROM users limit #{i}" } }
6+
sql { sequence(:sql) { |i| "SELECT * FROM users WHERE id > 0 LIMIT #{i}" } }
77
user
88
end
99

0 commit comments

Comments
 (0)