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..a23a1f83fb 100644 --- a/app/mailers/communications_mailer.rb +++ b/app/mailers/communications_mailer.rb @@ -1,9 +1,14 @@ class CommunicationsMailer < ApplicationMailer - def communication_email(to:, from:, subject:, body:, recipient:, sender:, unit:, rule:) + 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| + format.text { render plain: @body } + end + end + @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/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..86272b184a 100644 --- a/app/sidekiq/execute_communication_set_job.rb +++ b/app/sidekiq/execute_communication_set_job.rb @@ -164,10 +164,9 @@ 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, - rule: rule + sender: sender_user_for(unit) ).deliver_now { @@ -205,10 +204,9 @@ 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, - rule: rule + sender: sender_user_for(unit) ).deliver_now { 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/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" diff --git a/test/mailers/communications_mailer_test.rb b/test/mailers/communications_mailer_test.rb new file mode 100644 index 0000000000..692af180e2 --- /dev/null +++ b/test/mailers/communications_mailer_test.rb @@ -0,0 +1,34 @@ +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: Struct.new(:name).new('Convenor'), + 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')