From b70c3caa0767b315477c6cfa0bc932730906bc5e Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:09:12 +1100 Subject: [PATCH 1/4] feat: ability to send communication emails in plain text --- app/api/communication_rules_api.rb | 5 ++- .../entities/communication_action_entity.rb | 1 + app/mailers/communications_mailer.rb | 9 ++++- app/services/communication_transfer.rb | 2 +- app/sidekiq/execute_communication_set_job.rb | 2 ++ ...add_plain_text_to_communication_actions.rb | 5 +++ test/mailers/communications_mailer_test.rb | 36 +++++++++++++++++++ test/models/communication_transfer_test.rb | 11 ++++++ 8 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20261015040755_add_plain_text_to_communication_actions.rb create mode 100644 test/mailers/communications_mailer_test.rb diff --git a/app/api/communication_rules_api.rb b/app/api/communication_rules_api.rb index 8cb6335fcf..701610fa07 100644 --- a/app/api/communication_rules_api.rb +++ b/app/api/communication_rules_api.rb @@ -690,6 +690,7 @@ def sync_set_schedules!(communication_set, raw_schedules) requires :type, type: String optional :subject, type: String optional :body, type: String + optional :plain_text, type: Boolean optional :email_tutors, type: Boolean optional :email_convenors, type: Boolean optional :target_grade, type: Integer @@ -710,6 +711,7 @@ def sync_set_schedules!(communication_set, raw_schedules) :type, :subject, :body, + :plain_text, :email_tutors, :email_convenors, :target_grade, @@ -729,6 +731,7 @@ def sync_set_schedules!(communication_set, raw_schedules) optional :type, type: String optional :subject, type: String optional :body, type: String + optional :plain_text, type: Boolean optional :email_tutors, type: Boolean optional :email_convenors, type: Boolean optional :target_grade, type: Integer @@ -746,7 +749,7 @@ def sync_set_schedules!(communication_set, raw_schedules) action = rule.communication_actions.find(params[:id]) action_params = ActionController::Parameters.new(params) .require(:communication_action) - .permit(:type, :subject, :body, :email_tutors, :email_convenors, :target_grade, :task_definition_id) + .permit(:type, :subject, :body, :plain_text, :email_tutors, :email_convenors, :target_grade, :task_definition_id) action.update!(action_params) present action, with: Entities::CommunicationActionEntity diff --git a/app/api/entities/communication_action_entity.rb b/app/api/entities/communication_action_entity.rb index 1fb246634b..eaf6484a58 100644 --- a/app/api/entities/communication_action_entity.rb +++ b/app/api/entities/communication_action_entity.rb @@ -5,6 +5,7 @@ class CommunicationActionEntity < Grape::Entity expose :communication_rule_id expose :subject expose :body + expose :plain_text expose :email_tutors expose :email_convenors expose :target_grade diff --git a/app/mailers/communications_mailer.rb b/app/mailers/communications_mailer.rb index 95034342d9..52876b0163 100644 --- a/app/mailers/communications_mailer.rb +++ b/app/mailers/communications_mailer.rb @@ -1,5 +1,12 @@ class CommunicationsMailer < ApplicationMailer - def communication_email(to:, from:, subject:, body:, recipient:, sender:, unit:, rule:) + def communication_email(to:, from:, subject:, body:, recipient:, sender:, unit:, rule:, plain_text: false) + if plain_text + @body = body.to_s + return mail(to: to, from: from, subject: subject) do |format| + format.text { render plain: @body } + end + end + @recipient = recipient @sender = sender @unit = unit diff --git a/app/services/communication_transfer.rb b/app/services/communication_transfer.rb index 469c45e91f..760d04c155 100644 --- a/app/services/communication_transfer.rb +++ b/app/services/communication_transfer.rb @@ -19,7 +19,7 @@ class InvalidDocument < StandardError; end type operator target_grade task_statuses task_status_count task_target_grade activity_days spec_con_days submitted_portfolio ].freeze - ACTION_ATTRIBUTES = %w[type subject body email_tutors email_convenors target_grade].freeze + ACTION_ATTRIBUTES = %w[type subject body plain_text email_tutors email_convenors target_grade].freeze # Abbreviated references. Campuses are shared between units; everything else # is unit scoped. Group sets and groups are named rather than abbreviated, so diff --git a/app/sidekiq/execute_communication_set_job.rb b/app/sidekiq/execute_communication_set_job.rb index e645cb698f..eb1b4718e2 100644 --- a/app/sidekiq/execute_communication_set_job.rb +++ b/app/sidekiq/execute_communication_set_job.rb @@ -164,6 +164,7 @@ def execute_email_student_action(action, projects, unit, rule) from: sender, subject: subject, body: body, + plain_text: action.plain_text, recipient: recipient, sender: sender_user_for(unit), unit: unit, @@ -205,6 +206,7 @@ def execute_email_staff_action(action, projects, unit, rule) from: sender, subject: subject, body: body, + plain_text: action.plain_text, recipient: recipient, sender: sender_user_for(unit), unit: unit, diff --git a/db/migrate/20261015040755_add_plain_text_to_communication_actions.rb b/db/migrate/20261015040755_add_plain_text_to_communication_actions.rb new file mode 100644 index 0000000000..e370a5c179 --- /dev/null +++ b/db/migrate/20261015040755_add_plain_text_to_communication_actions.rb @@ -0,0 +1,5 @@ +class AddPlainTextToCommunicationActions < ActiveRecord::Migration[8.0] + def change + add_column :communication_actions, :plain_text, :boolean, default: false, null: false + end +end diff --git a/test/mailers/communications_mailer_test.rb b/test/mailers/communications_mailer_test.rb new file mode 100644 index 0000000000..01599ec949 --- /dev/null +++ b/test/mailers/communications_mailer_test.rb @@ -0,0 +1,36 @@ +require 'test_helper' + +class CommunicationsMailerTest < ActionMailer::TestCase + def test_plain_text_email_contains_only_the_entered_body + body = "Hello Ada,\n\nPlease review your work." + message = communication_email(body: body, plain_text: true) + + assert_match %r{\Atext/plain}, message.content_type + assert_nil message.html_part + assert_equal body, message.body.decoded.gsub("\r\n", "\n") + end + + def test_default_email_keeps_the_notification_layout + message = communication_email(body: 'Please review your work.') + + assert message.html_part + assert message.text_part + assert_includes message.html_part.body.decoded, 'Notification' + end + + private + + def communication_email(body:, plain_text: false) + CommunicationsMailer.communication_email( + to: 'student@example.com', + from: 'convenor@example.com', + subject: 'Reminder', + body: body, + recipient: nil, + sender: nil, + unit: nil, + rule: nil, + plain_text: plain_text + ) + end +end diff --git a/test/models/communication_transfer_test.rb b/test/models/communication_transfer_test.rb index 9fc8947f43..1957e4c41c 100644 --- a/test/models/communication_transfer_test.rb +++ b/test/models/communication_transfer_test.rb @@ -28,6 +28,17 @@ def test_importing_into_a_unit_with_the_same_task_repoints_the_reference assert_predicate imported, :executable? end + def test_plain_text_email_setting_survives_export_and_import + @rule.communication_actions.first.update!(plain_text: true) + document = CommunicationTransfer.export_set(@set) + target_unit = unit_with_task('T1.1') + + imported = CommunicationTransfer.import_set(document, target_unit) + + assert_equal true, document.dig('set', 'rules', 0, 'actions', 0, 'plain_text') + assert_predicate imported.communication_rules.first.communication_actions.first, :plain_text? + end + def test_importing_without_the_task_keeps_the_rule_but_blocks_the_set target_unit = unit_with_task('Different') From 97d74d2e526bdd2093275a29c7c26194a6b31701 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:13:09 +1100 Subject: [PATCH 2/4] chore: remove unused vars --- app/mailers/communications_mailer.rb | 4 +--- app/sidekiq/execute_communication_set_job.rb | 8 ++------ test/mailers/communications_mailer_test.rb | 2 -- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/app/mailers/communications_mailer.rb b/app/mailers/communications_mailer.rb index 52876b0163..a23a1f83fb 100644 --- a/app/mailers/communications_mailer.rb +++ b/app/mailers/communications_mailer.rb @@ -1,5 +1,5 @@ class CommunicationsMailer < ApplicationMailer - def communication_email(to:, from:, subject:, body:, recipient:, sender:, unit:, rule:, plain_text: false) + def communication_email(to:, from:, subject:, body:, recipient:, sender:, plain_text: false) if plain_text @body = body.to_s return mail(to: to, from: from, subject: subject) do |format| @@ -9,8 +9,6 @@ def communication_email(to:, from:, subject:, body:, recipient:, sender:, unit:, @recipient = recipient @sender = sender - @unit = unit - @rule = rule @body = body.to_s @body_paragraphs = @body.split(/\r?\n/).map(&:strip).reject(&:blank?) diff --git a/app/sidekiq/execute_communication_set_job.rb b/app/sidekiq/execute_communication_set_job.rb index eb1b4718e2..86272b184a 100644 --- a/app/sidekiq/execute_communication_set_job.rb +++ b/app/sidekiq/execute_communication_set_job.rb @@ -166,9 +166,7 @@ def execute_email_student_action(action, projects, unit, rule) body: body, plain_text: action.plain_text, recipient: recipient, - sender: sender_user_for(unit), - unit: unit, - rule: rule + sender: sender_user_for(unit) ).deliver_now { @@ -208,9 +206,7 @@ def execute_email_staff_action(action, projects, unit, rule) body: body, plain_text: action.plain_text, recipient: recipient, - sender: sender_user_for(unit), - unit: unit, - rule: rule + sender: sender_user_for(unit) ).deliver_now { diff --git a/test/mailers/communications_mailer_test.rb b/test/mailers/communications_mailer_test.rb index 01599ec949..066c26b9a3 100644 --- a/test/mailers/communications_mailer_test.rb +++ b/test/mailers/communications_mailer_test.rb @@ -28,8 +28,6 @@ def communication_email(body:, plain_text: false) body: body, recipient: nil, sender: nil, - unit: nil, - rule: nil, plain_text: plain_text ) end From 2f2bd06abffeb3637daf8b84045839d411952666 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:13:43 +1100 Subject: [PATCH 3/4] chore: run migration --- db/schema.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 7bbf1f038c..f1ed428448 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_09_16_003706) do +ActiveRecord::Schema[8.0].define(version: 2026_10_15_040755) do create_table "activity_types", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "abbreviation", null: false @@ -101,6 +101,7 @@ t.bigint "task_definition_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.boolean "plain_text", default: false, null: false t.index ["communication_rule_id"], name: "index_communication_actions_on_communication_rule_id" t.index ["task_definition_id"], name: "index_communication_actions_on_task_definition_id" t.index ["type"], name: "index_communication_actions_on_type" From 2698995c979208a8434e952b1208c7f1b2b41eb1 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:12:40 +1100 Subject: [PATCH 4/4] fix: test --- test/mailers/communications_mailer_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mailers/communications_mailer_test.rb b/test/mailers/communications_mailer_test.rb index 066c26b9a3..692af180e2 100644 --- a/test/mailers/communications_mailer_test.rb +++ b/test/mailers/communications_mailer_test.rb @@ -27,7 +27,7 @@ def communication_email(body:, plain_text: false) subject: 'Reminder', body: body, recipient: nil, - sender: nil, + sender: Struct.new(:name).new('Convenor'), plain_text: plain_text ) end