+ <%= form.label :feedback_message, "Reason for rejection:", class: "form-label" %>
+ <%= form.text_area :feedback_message, class: "form-control", rows: 4,
+ placeholder: "Please provide a reason for rejecting this extension request...",
+ aria_describedby: "rejectFeedbackHelpText",
+ required: true %>
+
+ This message will be sent to the student via email.
+
+
+
+
+ <% end %>
+
+
+
\ No newline at end of file
diff --git a/db/migrate/20260123233143_add_feedback_message_to_requests.rb b/db/migrate/20260123233143_add_feedback_message_to_requests.rb
new file mode 100644
index 00000000..ed7783ae
--- /dev/null
+++ b/db/migrate/20260123233143_add_feedback_message_to_requests.rb
@@ -0,0 +1,5 @@
+class AddFeedbackMessageToRequests < ActiveRecord::Migration[7.2]
+ def change
+ add_column :requests, :feedback_message, :text
+ end
+end
diff --git a/db/migrate/20260123233144_add_rejection_email_template_to_course_settings.rb b/db/migrate/20260123233144_add_rejection_email_template_to_course_settings.rb
new file mode 100644
index 00000000..9b6d13cd
--- /dev/null
+++ b/db/migrate/20260123233144_add_rejection_email_template_to_course_settings.rb
@@ -0,0 +1,6 @@
+class AddRejectionEmailTemplateToCourseSettings < ActiveRecord::Migration[7.2]
+ def change
+ add_column :course_settings, :rejection_email_subject, :string
+ add_column :course_settings, :rejection_email_template, :text
+ end
+end
diff --git a/docs/instructors.md b/docs/instructors.md
index 7e120468..31355f41 100644
--- a/docs/instructors.md
+++ b/docs/instructors.md
@@ -68,6 +68,21 @@ You can respond to requests in two ways:
3. Click **Approve** or **Reject** at the bottom.
4. The request status will update in real time.
+### Providing Feedback to Students
+
+When approving or rejecting requests, you can provide feedback that will be included in the email notification sent to the student:
+
+**Approving Requests:**
+- An optional feedback field allows you to provide additional context or instructions
+- Example: "Approved. Please ensure you submit by the extended deadline."
+- If no feedback is provided, a standard approval email will be sent
+
+**Rejecting Requests:**
+- A feedback field is **required** when rejecting a request
+- Provide a clear reason for the rejection to help students understand the decision
+- Example: "The requested due date is past the final exam. Please contact me during office hours to discuss alternatives."
+- This feedback will be included in the rejection email sent to the student
+
## Viewing Request History
To view all requests made in the course, click the **View all Requests** button at the top left.
@@ -139,7 +154,29 @@ Use provided dynamic variables to personalize each email.
`{{course_name}}, {{course_code}}, {{assignment_name}}`
- **Extension Information**
- `{{original_due_date}}, {{new_due_date}}, {{extension_days}}, {{status}}`
+ `{{original_due_date}}, {{new_due_date}}, {{extension_days}}, {{status}}, {{feedback_message}}`
+
+#### Rejection Email Template
+
+You can create a separate email template specifically for rejected requests. This allows you to customize the messaging when denying extension requests.
+
+To set up a custom rejection template:
+1. In the **Email Settings** section, configure the rejection email subject and body
+2. Include the `{{feedback_message}}` variable to insert the rejection reason you provided
+
+**Example Rejection Template:**
+```
+Hello {{student_name}},
+
+Your extension request for {{assignment_name}} in {{course_name}} ({{course_code}}) has been denied.
+
+Reason for rejection: {{feedback_message}}
+
+If you have any questions or would like to discuss this decision, please reach out to your course staff.
+
+Thank you,
+{{course_name}} Staff
+```
Click **Reset to Default** to restore the system default template.
diff --git a/spec/controllers/requests_controller_spec.rb b/spec/controllers/requests_controller_spec.rb
index 810033d2..9c5ba05b 100644
--- a/spec/controllers/requests_controller_spec.rb
+++ b/spec/controllers/requests_controller_spec.rb
@@ -329,6 +329,16 @@
expect(flash[:notice]).to match(/approved/i)
end
+ it 'passes feedback_message to approve method when provided' do
+ expect_any_instance_of(Request).to receive(:approve).with(
+ anything, anything, hash_including(feedback_message: 'Great work!')
+ ).and_return(true)
+
+ post :approve, params: { course_id: course.id, id: request.id, feedback_message: 'Great work!' }
+
+ expect(response).to redirect_to(course_requests_path(course))
+ end
+
# it 'shows error if approval fails' do
# # stub the *same* request to return false here
# allow(request).to receive(:approve).and_return(false)
@@ -353,6 +363,16 @@
expect(flash[:notice]).to match(/denied/i)
end
+ it 'passes feedback_message to reject method when provided' do
+ expect_any_instance_of(Request).to receive(:reject).with(
+ anything, hash_including(feedback_message: 'Cannot grant extension.')
+ ).and_return(true)
+
+ post :reject, params: { course_id: course.id, id: request.id, feedback_message: 'Cannot grant extension.' }
+
+ expect(response).to redirect_to(course_requests_path(course))
+ end
+
it 'shows error if rejection fails' do
allow_any_instance_of(Request).to receive(:reject).and_return(false)
post :reject, params: { course_id: course.id, id: request.id }
diff --git a/spec/models/request_spec.rb b/spec/models/request_spec.rb
index 9e4156f3..7cef47ea 100644
--- a/spec/models/request_spec.rb
+++ b/spec/models/request_spec.rb
@@ -512,6 +512,17 @@
expect(request.status).to eq('approved')
end
end
+
+ it 'saves feedback_message when provided' do
+ feedback = 'Approved with a note to complete on time next time.'
+ request.approve(lms_facade, instructor, feedback_message: feedback)
+ expect(request.reload.feedback_message).to eq(feedback)
+ end
+
+ it 'allows nil feedback_message' do
+ request.approve(lms_facade, instructor, feedback_message: nil)
+ expect(request.reload.feedback_message).to be_nil
+ end
end
describe '#reject' do
@@ -524,6 +535,17 @@
request.reject(instructor)
expect(request.last_processed_by_user_id).to eq(instructor.id)
end
+
+ it 'saves feedback_message when provided' do
+ feedback = 'We cannot approve this request because the deadline is too close.'
+ request.reject(instructor, feedback_message: feedback)
+ expect(request.reload.feedback_message).to eq(feedback)
+ end
+
+ it 'allows nil feedback_message' do
+ request.reject(instructor, feedback_message: nil)
+ expect(request.reload.feedback_message).to be_nil
+ end
end
describe '#send_email_response' do
@@ -581,5 +603,48 @@
expect(EmailService).not_to receive(:send_email)
request.send_email_response
end
+
+ context 'when request is denied' do
+ let(:rejection_template) do
+ <<~TEMPLATE
+ Dear {{student_name}},
+ Your extension request has been {{status}}.
+ Reason: {{feedback_message}}
+ TEMPLATE
+ end
+
+ before do
+ course_settings.update(
+ rejection_email_subject: 'Rejection: {{course_code}}',
+ rejection_email_template: rejection_template
+ )
+ request.update(status: 'denied', feedback_message: 'Unable to grant extension at this time.')
+ end
+
+ it 'uses rejection email template for denied requests' do
+ expect(EmailService).to receive(:send_email).with(
+ hash_including(
+ subject_template: 'Rejection: {{course_code}}',
+ body_template: rejection_template,
+ mapping: hash_including(
+ 'feedback_message' => 'Unable to grant extension at this time.'
+ )
+ )
+ )
+ request.send_email_response
+ end
+ end
+
+ it 'includes feedback_message in mapping with default message when nil' do
+ request.update(feedback_message: nil)
+ expect(EmailService).to receive(:send_email).with(
+ hash_including(
+ mapping: hash_including(
+ 'feedback_message' => 'No additional feedback provided.'
+ )
+ )
+ )
+ request.send_email_response
+ end
end
end