Skip to content
This repository was archived by the owner on Jul 14, 2025. It is now read-only.

Commit cbae98f

Browse files
LhcflDrenmi
andauthored
FEATURE: Allows CSV file result to be attached in automated PMs (#318)
This commit adds an optional setting that allows to attach query results in CSV format as a file to PMs sent by Data Explorer's automation scripts. meta topic: https://meta.discourse.org/t/turn-data-explorer-query-results-into-csv-to-attach-to-discourse-automated-emails/267529 Co-authored-by: Drenmi <drenmi@gmail.com>
1 parent 68760cd commit cbae98f

File tree

7 files changed

+157
-45
lines changed

7 files changed

+157
-45
lines changed

app/controllers/discourse_data_explorer/query_controller.rb

Lines changed: 17 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -179,49 +179,27 @@ def run
179179

180180
render json: { success: false, errors: [err_msg] }, status: 422
181181
else
182-
pg_result = result[:pg_result]
183-
cols = pg_result.fields
182+
content_disposition =
183+
"attachment; filename=#{query.slug}@#{Slug.for(Discourse.current_hostname, "discourse")}-#{Date.today}.dcqresult"
184+
184185
respond_to do |format|
185186
format.json do
186-
if params[:download]
187-
response.headers[
188-
"Content-Disposition"
189-
] = "attachment; filename=#{query.slug}@#{Slug.for(Discourse.current_hostname, "discourse")}-#{Date.today}.dcqresult.json"
190-
end
191-
json = {
192-
success: true,
193-
errors: [],
194-
duration: (result[:duration_secs].to_f * 1000).round(1),
195-
result_count: pg_result.values.length || 0,
196-
params: query_params,
197-
columns: cols,
198-
default_limit: SiteSetting.data_explorer_query_result_limit,
199-
}
200-
json[:explain] = result[:explain] if opts[:explain]
201-
202-
if !params[:download]
203-
relations, colrender = DataExplorer.add_extra_data(pg_result)
204-
json[:relations] = relations
205-
json[:colrender] = colrender
206-
end
207-
208-
json[:rows] = pg_result.values
209-
210-
render json: json
187+
response.headers["Content-Disposition"] = "#{content_disposition}.json" if params[
188+
:download
189+
]
190+
191+
render json:
192+
ResultFormatConverter.convert(
193+
:json,
194+
result,
195+
query_params:,
196+
download: params[:download],
197+
)
211198
end
212199
format.csv do
213-
response.headers[
214-
"Content-Disposition"
215-
] = "attachment; filename=#{query.slug}@#{Slug.for(Discourse.current_hostname, "discourse")}-#{Date.today}.dcqresult.csv"
216-
217-
require "csv"
218-
text =
219-
CSV.generate do |csv|
220-
csv << cols
221-
pg_result.values.each { |row| csv << row }
222-
end
223-
224-
render plain: text
200+
response.headers["Content-Disposition"] = "#{content_disposition}.csv"
201+
202+
render plain: ResultFormatConverter.convert(:csv, result)
225203
end
226204
end
227205
end

config/locales/client.en.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,5 @@ en:
116116
label: Data Explorer Query parameters
117117
skip_empty:
118118
label: Skip sending PM if there are no results
119+
attach_csv:
120+
label: Attach the CSV file to the PM

lib/report_generator.rb

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ def self.generate(query_id, query_params, recipients, opts = {})
99
recipients = filter_recipients_by_query_access(recipients, query)
1010
params = params_to_hash(query_params)
1111

12-
result = DataExplorer.run_query(query, params)[:pg_result]
12+
result = DataExplorer.run_query(query, params)
1313
query.update!(last_run_at: Time.now)
1414

15-
return [] if opts[:skip_empty] && result.values.empty?
16-
table = ResultToMarkdown.convert(result)
15+
return [] if opts[:skip_empty] && result[:pg_result].values.empty?
16+
table = ResultToMarkdown.convert(result[:pg_result])
1717

18-
build_report_pms(query, table, recipients)
18+
build_report_pms(query, table, recipients, attach_csv: opts[:attach_csv], result:)
1919
end
2020

2121
private
@@ -40,8 +40,20 @@ def self.params_to_hash(query_params)
4040
params_hash
4141
end
4242

43-
def self.build_report_pms(query, table = "", targets = [])
43+
def self.build_report_pms(query, table = "", targets = [], attach_csv: false, result: nil)
4444
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
56+
4557
targets.each do |target|
4658
name = target[0]
4759
pm_type = "target_#{target[1]}s"
@@ -53,6 +65,9 @@ def self.build_report_pms(query, table = "", targets = [])
5365
"Query Name:\n#{query.name}\n\nHere are the results:\n#{table}\n\n" +
5466
"<a href='#{Discourse.base_url}/admin/plugins/explorer?id=#{query.id}'>View query in Data Explorer</a>\n\n" +
5567
"Report created at #{Time.zone.now.strftime("%Y-%m-%d at %H:%M:%S")} (#{Time.zone.name})"
68+
if upload
69+
pm["raw"] << "\n\nAppendix: [#{upload.original_filename}|attachment](#{upload.short_url})"
70+
end
5671
pms << pm
5772
end
5873
pms

lib/result_format_converter.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
module ::DiscourseDataExplorer
3+
class ResultFormatConverter
4+
def self.convert(file_type, result, opts = {})
5+
self.new(result, opts).send("to_#{file_type}")
6+
end
7+
8+
def initialize(result, opts)
9+
@result = result
10+
@opts = opts
11+
end
12+
13+
private
14+
15+
attr_reader :result
16+
attr_reader :opts
17+
18+
def pg_result
19+
@pg_result ||= @result[:pg_result]
20+
end
21+
22+
def cols
23+
@cols ||= pg_result.fields
24+
end
25+
26+
def to_csv
27+
require "csv"
28+
CSV.generate do |csv|
29+
csv << cols
30+
pg_result.values.each { |row| csv << row }
31+
end
32+
end
33+
34+
def to_json
35+
json = {
36+
success: true,
37+
errors: [],
38+
duration: (result[:duration_secs].to_f * 1000).round(1),
39+
result_count: pg_result.values.length || 0,
40+
params: opts[:query_params],
41+
columns: cols,
42+
default_limit: SiteSetting.data_explorer_query_result_limit,
43+
}
44+
json[:explain] = result[:explain] if opts[:explain]
45+
46+
if !opts[:download]
47+
relations, colrender = DataExplorer.add_extra_data(pg_result)
48+
json[:relations] = relations
49+
json[:colrender] = colrender
50+
end
51+
52+
json[:rows] = pg_result.values
53+
54+
json
55+
end
56+
57+
#TODO: we can move ResultToMarkdown here
58+
end
59+
end

plugin.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ module ::DiscourseDataExplorer
7979

8080
require_relative "lib/report_generator"
8181
require_relative "lib/result_to_markdown"
82+
require_relative "lib/result_format_converter"
8283
reloadable_patch do
8384
if defined?(DiscourseAutomation)
8485
add_automation_scriptable("recurring_data_explorer_result_pm") do
@@ -90,6 +91,7 @@ module ::DiscourseDataExplorer
9091
field :query_id, component: :choices, required: true, extra: { content: queries }
9192
field :query_params, component: :"key-value", accepts_placeholders: true
9293
field :skip_empty, component: :boolean
94+
field :attach_csv, component: :boolean
9395

9496
version 1
9597
triggerables [:recurring]
@@ -99,6 +101,7 @@ module ::DiscourseDataExplorer
99101
query_id = fields.dig("query_id", "value")
100102
query_params = fields.dig("query_params", "value") || {}
101103
skip_empty = fields.dig("skip_empty", "value") || false
104+
attach_csv = fields.dig("attach_csv", "value") || false
102105

103106
unless SiteSetting.data_explorer_enabled
104107
Rails.logger.warn "#{DiscourseDataExplorer::PLUGIN_NAME} - plugin must be enabled to run automation #{automation.id}"
@@ -111,7 +114,7 @@ module ::DiscourseDataExplorer
111114
end
112115

113116
DiscourseDataExplorer::ReportGenerator
114-
.generate(query_id, query_params, recipients, { skip_empty: })
117+
.generate(query_id, query_params, recipients, { skip_empty:, attach_csv: })
115118
.each do |pm|
116119
begin
117120
utils.send_pm(pm, automation_id: automation.id, prefers_encrypt: false)

spec/report_generator_spec.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,5 +130,25 @@
130130
expect(result[1]["target_group_names"]).to eq([group.name])
131131
expect(result[2]["target_emails"]).to eq(["john@doe.com"])
132132
end
133+
134+
it "works with attached csv file" do
135+
SiteSetting.personal_message_enabled_groups = group.id
136+
DiscourseDataExplorer::ResultToMarkdown.expects(:convert).returns("le table")
137+
freeze_time
138+
139+
result =
140+
described_class.generate(query.id, query_params, [user.username], { attach_csv: true })
141+
142+
filename =
143+
"#{query.slug}@#{Slug.for(Discourse.current_hostname, "discourse")}-#{Date.today}.dcqresult.csv"
144+
145+
expect(result[0]["raw"]).to include(
146+
"Hi #{user.username}, your data explorer report is ready.\n\n" +
147+
"Query Name:\n#{query.name}\n\nHere are the results:\nle table\n\n" +
148+
"<a href='#{Discourse.base_url}/admin/plugins/explorer?id=#{query.id}'>View query in Data Explorer</a>\n\n" +
149+
"Report created at #{Time.zone.now.strftime("%Y-%m-%d at %H:%M:%S")} (#{Time.zone.name})\n\n" +
150+
"Appendix: [#{filename}|attachment](upload://",
151+
)
152+
end
133153
end
134154
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
describe DiscourseDataExplorer::ResultFormatConverter do
4+
fab!(:user)
5+
fab!(:post)
6+
fab!(:query) { DiscourseDataExplorer::Query.find(-1) }
7+
8+
let(:query_params) { [{ from_days_ago: 0 }, { duration_days: 15 }] }
9+
let(:query_result) { DiscourseDataExplorer::DataExplorer.run_query(query, query_params) }
10+
11+
before { SiteSetting.data_explorer_enabled = true }
12+
13+
describe ".convert" do
14+
context "for csv files" do
15+
it "format results as a csv table with headers and columns" do
16+
result = described_class.convert(:csv, query_result)
17+
18+
table = <<~CSV
19+
liker_user_id,liked_user_id,count
20+
CSV
21+
22+
expect(result).to include(table)
23+
end
24+
end
25+
26+
context "for json files" do
27+
it "format results as a json file" do
28+
result = described_class.convert(:json, query_result, { query_params: })
29+
30+
expect(result[:columns]).to contain_exactly("liker_user_id", "liked_user_id", "count")
31+
expect(result[:params]).to eq(query_params)
32+
end
33+
end
34+
end
35+
end

0 commit comments

Comments
 (0)