FIX: send single report PM to groups (#284)

The main change here is that we now send a single PM to a group rather than individual PMs to each group member.

This change also adds email recipients correctly as target_emails since support was added within Discourse Automation.
This commit is contained in:
David Battersby 2024-03-27 17:40:26 +08:00 committed by GitHub
parent 2f1044820c
commit cde34fb316
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 113 additions and 61 deletions

View File

@ -2,19 +2,11 @@
module ::DiscourseDataExplorer module ::DiscourseDataExplorer
class ReportGenerator class ReportGenerator
def initialize(creator_user_id) def self.generate(query_id, query_params, recipients)
@creator_user_id = creator_user_id
end
def generate(query_id, query_params, recipients)
query = DiscourseDataExplorer::Query.find(query_id) query = DiscourseDataExplorer::Query.find(query_id)
return [] unless query return [] if !query || recipients.empty?
return [] if recipients.empty?
creator = User.find_by(id: @creator_user_id) recipients = filter_recipients_by_query_access(recipients, query)
return [] unless Guardian.new(creator).can_send_private_messages?
usernames = filter_recipients_by_query_access(recipients, query)
params = params_to_hash(query_params) params = params_to_hash(query_params)
result = DataExplorer.run_query(query, params) result = DataExplorer.run_query(query, params)
@ -22,10 +14,12 @@ module ::DiscourseDataExplorer
table = ResultToMarkdown.convert(result[:pg_result]) table = ResultToMarkdown.convert(result[:pg_result])
build_report_pms(query, table, usernames) build_report_pms(query, table, recipients)
end end
def params_to_hash(query_params) private
def self.params_to_hash(query_params)
params = JSON.parse(query_params) params = JSON.parse(query_params)
params_hash = {} params_hash = {}
@ -45,13 +39,16 @@ module ::DiscourseDataExplorer
params_hash params_hash
end end
def build_report_pms(query, table = "", usernames = []) def self.build_report_pms(query, table = "", targets = [])
pms = [] pms = []
usernames.flatten.compact.uniq.each do |username| targets.each do |target|
name = target[0]
pm_type = "target_#{target[1]}s"
pm = {} pm = {}
pm["title"] = "Scheduled Report for #{query.name}" pm["title"] = "Scheduled Report for #{query.name}"
pm["target_usernames"] = Array(username) pm[pm_type] = Array(name)
pm["raw"] = "Hi #{username}, your data explorer report is ready.\n\n" + pm["raw"] = "Hi #{name}, your data explorer report is ready.\n\n" +
"Query Name:\n#{query.name}\n\nHere are the results:\n#{table}\n\n" + "Query Name:\n#{query.name}\n\nHere are the results:\n#{table}\n\n" +
"<a href='#{Discourse.base_url}/admin/plugins/explorer?id=#{query.id}'>View query in Data Explorer</a>\n\n" + "<a href='#{Discourse.base_url}/admin/plugins/explorer?id=#{query.id}'>View query in Data Explorer</a>\n\n" +
"Report created at #{Time.zone.now.strftime("%Y-%m-%d at %H:%M:%S")} (#{Time.zone.name})" "Report created at #{Time.zone.now.strftime("%Y-%m-%d at %H:%M:%S")} (#{Time.zone.name})"
@ -60,23 +57,25 @@ module ::DiscourseDataExplorer
pms pms
end end
private def self.filter_recipients_by_query_access(recipients, query)
users = User.where(username: recipients)
groups = Group.where(name: recipients)
emails = recipients - users.pluck(:username) - groups.pluck(:name)
result = []
def filter_recipients_by_query_access(recipients, query) users.each do |user|
recipients.reduce([]) do |names, recipient| result << [user.username, "username"] if Guardian.new(user).user_can_access_query?(query)
if (group = Group.find_by(name: recipient)) &&
(
group.id == Group::AUTO_GROUPS[:admins] ||
query.query_groups.exists?(group_id: group.id)
)
names.concat group.users.pluck(:username)
elsif (user = User.find_by(username: recipient)) &&
Guardian.new(user).user_can_access_query?(query)
names << recipient
end
names
end end
groups.each do |group|
if group.id == Group::AUTO_GROUPS[:admins] || query.query_groups.exists?(group_id: group.id)
result << [group.name, "group_name"]
end
end
emails.each { |email| result << [email, "email"] if Email.is_valid?(email) }
result
end end
end end
end end

View File

@ -94,7 +94,7 @@ after_initialize do
triggerables [:recurring] triggerables [:recurring]
script do |_, fields, automation| script do |_, fields, automation|
recipients = Array(fields.dig("recipients", "value")) recipients = Array(fields.dig("recipients", "value")).uniq
query_id = fields.dig("query_id", "value") query_id = fields.dig("query_id", "value")
query_params = fields.dig("query_params", "value") || {} query_params = fields.dig("query_params", "value") || {}
@ -108,17 +108,15 @@ after_initialize do
next next
end end
data_explorer_report = DiscourseDataExplorer::ReportGenerator
DiscourseDataExplorer::ReportGenerator.new(automation.last_updated_by_id) .generate(query_id, query_params, recipients)
report_pms = data_explorer_report.generate(query_id, query_params, recipients) .each do |pm|
begin
report_pms.each do |pm| utils.send_pm(pm, automation_id: automation.id, prefers_encrypt: false)
begin rescue ActiveRecord::RecordNotSaved => e
utils.send_pm(pm, automation_id: automation.id) Rails.logger.warn "#{DiscourseDataExplorer::PLUGIN_NAME} - couldn't send PM for automation #{automation.id}: #{e.message}"
rescue ActiveRecord::RecordNotSaved => e end
Rails.logger.warn "#{DiscourseDataExplorer::PLUGIN_NAME} - couldn't send PM for automation #{automation.id}: #{e.message}"
end end
end
end end
end end
end end

View File

@ -60,17 +60,20 @@ describe "RecurringDataExplorerResultPm" do
automation.trigger! automation.trigger!
end.to change { Topic.count }.by(3) end.to change { Topic.count }.by(3)
created_topics = Topic.last(3) user_topics = Topic.first(2)
expect(created_topics.pluck(:archetype)).to eq( group_topics = Topic.last(1)
expect(Topic.last(3).pluck(:archetype)).to eq(
[Archetype.private_message, Archetype.private_message, Archetype.private_message], [Archetype.private_message, Archetype.private_message, Archetype.private_message],
) )
expect(created_topics.map { |t| t.allowed_users.pluck(:username).sort }).to match_array( expect(user_topics.map { |t| t.allowed_users.pluck(:username).sort }).to match_array(
[ [
[user.username, Discourse.system_user.username], [user.username, Discourse.system_user.username],
[another_user.username, Discourse.system_user.username], [another_user.username, Discourse.system_user.username],
[group_user.username, Discourse.system_user.username],
], ],
) )
expect(group_topics.map { |t| t.allowed_groups.pluck(:name).sort }).to match_array(
[[another_group.name]],
)
end end
it "has appropriate content from the report generator" do it "has appropriate content from the report generator" do
@ -78,7 +81,7 @@ describe "RecurringDataExplorerResultPm" do
automation.trigger! automation.trigger!
expect(Post.last.raw).to include( expect(Post.last.raw).to include(
"Hi #{group_user.username}, your data explorer report is ready.\n\nQuery Name:\n#{query.name}", "Hi #{another_group.name}, your data explorer report is ready.\n\nQuery Name:\n#{query.name}",
) )
end end
end end

View File

@ -15,16 +15,10 @@ describe DiscourseDataExplorer::ReportGenerator do
before { SiteSetting.data_explorer_enabled = true } before { SiteSetting.data_explorer_enabled = true }
describe ".generate" do describe ".generate" do
it "returns [] if the creator cannot send PMs" do
result = described_class.new(user.id).generate(query.id, query_params, [user.username])
expect(result).to eq []
end
it "returns [] if the recipient is not in query group" do it "returns [] if the recipient is not in query group" do
Fabricate(:query_group, query: query, group: group) Fabricate(:query_group, query: query, group: group)
result = result =
described_class.new(user.id).generate( described_class.generate(
query.id, query.id,
query_params, query_params,
[unauthorised_user.username, unauthorised_group.name], [unauthorised_user.username, unauthorised_group.name],
@ -38,7 +32,7 @@ describe DiscourseDataExplorer::ReportGenerator do
DiscourseDataExplorer::ResultToMarkdown.expects(:convert).returns("le table") DiscourseDataExplorer::ResultToMarkdown.expects(:convert).returns("le table")
freeze_time freeze_time
result = described_class.new(user.id).generate(query.id, query_params, [user.username]) result = described_class.generate(query.id, query_params, [user.username])
expect(result).to eq( expect(result).to eq(
[ [
@ -56,21 +50,21 @@ describe DiscourseDataExplorer::ReportGenerator do
end end
it "still returns a list of pms if a group or user does not exist" do it "still returns a list of pms if a group or user does not exist" do
admin = Fabricate(:admin) Fabricate(:query_group, query: query, group: group)
SiteSetting.personal_message_enabled_groups = group.id SiteSetting.personal_message_enabled_groups = group.id
DiscourseDataExplorer::ResultToMarkdown.expects(:convert).returns("le table") DiscourseDataExplorer::ResultToMarkdown.expects(:convert).returns("le table")
freeze_time freeze_time
result = result = described_class.generate(query.id, query_params, [group.name, "non-existent-group"])
described_class.new(user.id).generate(query.id, query_params, %w[admins non-existent-group])
expect(result).to eq( expect(result).to eq(
[ [
{ {
"title" => "Scheduled Report for #{query.name}", "title" => "Scheduled Report for #{query.name}",
"target_usernames" => [admin.username], "target_group_names" => [group.name],
"raw" => "raw" =>
"Hi #{admin.username}, your data explorer report is ready.\n\n" + "Hi #{group.name}, your data explorer report is ready.\n\n" +
"Query Name:\n#{query.name}\n\nHere are the results:\nle table\n\n" + "Query Name:\n#{query.name}\n\nHere are the results:\nle table\n\n" +
"<a href='#{Discourse.base_url}/admin/plugins/explorer?id=#{query.id}'>View query in Data Explorer</a>\n\n" + "<a href='#{Discourse.base_url}/admin/plugins/explorer?id=#{query.id}'>View query in Data Explorer</a>\n\n" +
"Report created at #{Time.zone.now.strftime("%Y-%m-%d at %H:%M:%S")} (#{Time.zone.name})", "Report created at #{Time.zone.now.strftime("%Y-%m-%d at %H:%M:%S")} (#{Time.zone.name})",
@ -78,5 +72,63 @@ describe DiscourseDataExplorer::ReportGenerator do
], ],
) )
end end
it "works with email recipients" do
DiscourseDataExplorer::ResultToMarkdown.expects(:convert).returns("le table")
email = "john@doe.com"
result = described_class.generate(query.id, query_params, [email])
expect(result).to eq(
[
{
"title" => "Scheduled Report for #{query.name}",
"target_emails" => [email],
"raw" =>
"Hi #{email}, your data explorer report is ready.\n\n" +
"Query Name:\n#{query.name}\n\nHere are the results:\nle table\n\n" +
"<a href='#{Discourse.base_url}/admin/plugins/explorer?id=#{query.id}'>View query in Data Explorer</a>\n\n" +
"Report created at #{Time.zone.now.strftime("%Y-%m-%d at %H:%M:%S")} (#{Time.zone.name})",
},
],
)
end
it "works with duplicate recipients" do
DiscourseDataExplorer::ResultToMarkdown.expects(:convert).returns("table data")
result = described_class.generate(query.id, query_params, [user.username, user.username])
expect(result).to eq(
[
{
"title" => "Scheduled Report for #{query.name}",
"target_usernames" => [user.username],
"raw" =>
"Hi #{user.username}, your data explorer report is ready.\n\n" +
"Query Name:\n#{query.name}\n\nHere are the results:\ntable data\n\n" +
"<a href='#{Discourse.base_url}/admin/plugins/explorer?id=#{query.id}'>View query in Data Explorer</a>\n\n" +
"Report created at #{Time.zone.now.strftime("%Y-%m-%d at %H:%M:%S")} (#{Time.zone.name})",
},
],
)
end
it "works with multiple recipient types" do
Fabricate(:query_group, query: query, group: group)
DiscourseDataExplorer::ResultToMarkdown.expects(:convert).returns("table data")
result =
described_class.generate(
query.id,
query_params,
[group.name, user.username, "john@doe.com"],
)
expect(result.length).to eq(3)
expect(result[0]["target_usernames"]).to eq([user.username])
expect(result[1]["target_group_names"]).to eq([group.name])
expect(result[2]["target_emails"]).to eq(["john@doe.com"])
end
end end
end end