Skip to content

Commit dfb4e36

Browse files
feat(notifications): send notifications when extensions are granted
Implemented backend logic to send emails to tutor and student when extensions are granted. Also enable it so the front end can use the returned information from the api to display notifications.
1 parent ae10827 commit dfb4e36

File tree

6 files changed

+435
-2
lines changed

6 files changed

+435
-2
lines changed

app/api/staff_grant_extension_api.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,34 @@ class StaffGrantExtensionApi < Grape::API
115115
error!({ error: 'Some extensions failed to be granted', results: results }, 400)
116116
end
117117

118+
# Send notifications only if successful and after processing all students
119+
if results[:successful].any?
120+
successful_extensions = results[:successful].map do |result|
121+
# Re-fetch project within the transaction to ensure consistency
122+
project = Project.find(result[:project_id])
123+
task = project.task_for_task_definition(task_definition)
124+
# Ensure we get the latest extension comment created within this transaction
125+
task.all_comments.where(content_type: :extension).order(created_at: :desc).first
126+
end
127+
128+
# Filter out any nil results in case a comment wasn't found (shouldn't happen ideally)
129+
successful_extensions.compact!
130+
131+
if successful_extensions.any?
132+
NotificationsMailer.extension_granted(
133+
successful_extensions,
134+
current_user,
135+
params[:student_ids].count,
136+
results[:failed],
137+
true # is_staff_grant = true
138+
).deliver_later
139+
end
140+
end
141+
118142
status 201
119143
present results, with: Grape::Presenters::Presenter
120144
end
145+
121146
rescue ActiveRecord::RecordNotFound
122147
error!({ error: 'Unit or task definition not found' }, 404)
123148
rescue StandardError

app/mailers/notifications_mailer.rb

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
class NotificationsMailer < ActionMailer::Base
2+
3+
# Load configuration values at class level
4+
def self.doubtfire_host
5+
Doubtfire::Application.config.institution[:host] || 'doubtfire.deakin.edu.au'
6+
end
7+
8+
def self.doubtfire_product_name
9+
Doubtfire::Application.config.institution[:product_name] || 'Doubtfire'
10+
end
11+
12+
# Set default from address using class methods
13+
default from: -> { "#{self.class.doubtfire_product_name} <#{@granted_by&.email}>" }
14+
215
def add_general
3-
@doubtfire_host = Doubtfire::Application.config.institution[:host]
4-
@doubtfire_product_name = Doubtfire::Application.config.institution[:product_name]
16+
@doubtfire_host = self.class.doubtfire_host
17+
@doubtfire_product_name = self.class.doubtfire_product_name
518
@unsubscribe_url = "#{@doubtfire_host}/#/home?notifications"
619
end
720

@@ -88,6 +101,68 @@ def this_these(num)
88101
end
89102
end
90103

104+
# Sends a summary email to the staff member who granted the extensions
105+
def extension_granted_summary(extensions, granted_by, total_selected, failed_extensions = [])
106+
@granted_by = granted_by
107+
@extensions = extensions
108+
@total_selected = total_selected
109+
@failed_extensions = failed_extensions
110+
@unit = extensions.any? ? extensions.first.task.unit : nil
111+
@is_tutor = true
112+
113+
add_general
114+
115+
email_with_name = %("#{@granted_by.name}" <#{@granted_by.email}>)
116+
mail(
117+
to: email_with_name,
118+
subject: @unit ? "#{@unit.name}: Staff Grant Extensions" : "Staff Grant Extensions",
119+
template_name: 'extension_granted'
120+
)
121+
end
122+
123+
# Sends a notification to a student about their granted extension
124+
def extension_granted_notification(extension, granted_by)
125+
@granted_by = granted_by
126+
@extension = extension
127+
@task = extension.task
128+
@student = extension.project.student
129+
@is_tutor = false
130+
131+
add_general
132+
133+
email_with_name = %("#{@student.name}" <#{@student.email}>)
134+
tutor_email = %("#{@granted_by.name}" <#{@granted_by.email}>)
135+
136+
mail(
137+
to: email_with_name,
138+
from: tutor_email,
139+
subject: "#{@task.unit.name}: Extension granted for #{@task.task_definition.name}",
140+
template_name: 'extension_granted'
141+
)
142+
end
143+
144+
# Main method to handle extension notifications from staff
145+
def extension_granted(extensions, granted_by, total_selected, failed_extensions = [], is_staff_grant = false)
146+
# Only send notifications for staff-granted bulk extensions
147+
return unless is_staff_grant && (extensions.any? || failed_extensions.any?)
148+
149+
begin
150+
# Send summary to staff member who granted the extensions
151+
extension_granted_summary(extensions, granted_by, total_selected, failed_extensions).deliver_now
152+
153+
# Send individual notifications only to students who have enabled email notifications
154+
extensions.each do |extension|
155+
student = extension.project.student
156+
if student.receive_task_notifications
157+
extension_granted_notification(extension, granted_by).deliver_now
158+
end
159+
end
160+
rescue => e
161+
Rails.logger.error "Failed to send extension notifications: #{e.message}"
162+
Rails.logger.error e.backtrace.join("\n")
163+
end
164+
end
165+
91166
helper_method :top_task_desc
92167
helper_method :were_was
93168
helper_method :are_is
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
5+
<style>
6+
body {
7+
font-family: Arial, sans-serif;
8+
line-height: 1.6;
9+
color: #333;
10+
max-width: 600px;
11+
margin: 0 auto;
12+
padding: 20px;
13+
}
14+
.header {
15+
background-color: #f8f9fa;
16+
padding: 20px;
17+
border-radius: 5px;
18+
margin-bottom: 20px;
19+
}
20+
.content {
21+
padding: 20px;
22+
}
23+
.footer {
24+
margin-top: 20px;
25+
padding-top: 20px;
26+
border-top: 1px solid #eee;
27+
font-size: 0.9em;
28+
color: #666;
29+
}
30+
table {
31+
width: 100%;
32+
border-collapse: collapse;
33+
margin: 20px 0;
34+
}
35+
th, td {
36+
padding: 12px;
37+
text-align: left;
38+
border-bottom: 1px solid #ddd;
39+
}
40+
th {
41+
background-color: #f8f9fa;
42+
}
43+
.success {
44+
color: #28a745;
45+
}
46+
.error {
47+
color: #dc3545;
48+
}
49+
</style>
50+
</head>
51+
<body>
52+
<div class="header">
53+
<h2>Extension Granted</h2>
54+
</div>
55+
56+
<div class="content">
57+
<% if @is_tutor %>
58+
<p>You have granted extensions for the following students:</p>
59+
60+
<table>
61+
<thead>
62+
<tr>
63+
<th>Student</th>
64+
<th>Task</th>
65+
<th>New Due Date</th>
66+
</tr>
67+
</thead>
68+
<tbody>
69+
<% @extensions.each do |extension| %>
70+
<tr>
71+
<td><%= extension.project.student.name %></td>
72+
<td><%= extension.task.task_definition.name %></td>
73+
<td><%= extension.task.due_date.strftime("%d %b %Y") %></td>
74+
</tr>
75+
<% end %>
76+
</tbody>
77+
</table>
78+
79+
<% if @failed_extensions.any? %>
80+
<h3>Failed Extensions</h3>
81+
<table>
82+
<thead>
83+
<tr>
84+
<th>Student ID</th>
85+
<th>Error</th>
86+
</tr>
87+
</thead>
88+
<tbody>
89+
<% @failed_extensions.each do |failed| %>
90+
<tr>
91+
<td><%= failed[:student_id] %></td>
92+
<td class="error"><%= failed[:error] %></td>
93+
</tr>
94+
<% end %>
95+
</tbody>
96+
</table>
97+
<% end %>
98+
99+
<p>Total students selected: <%= @total_selected %></p>
100+
<p>Successfully granted: <%= @extensions.count %></p>
101+
<% if @failed_extensions.any? %>
102+
<p>Failed: <%= @failed_extensions.count %></p>
103+
<% end %>
104+
<% else %>
105+
<p>Dear <%= @student.name %>,</p>
106+
107+
<p>An extension has been granted for your task: <strong><%= @task.task_definition.name %></strong></p>
108+
109+
<p>Details:</p>
110+
<ul>
111+
<li>New due date: <%= @task.due_date.strftime("%d %b %Y") %></li>
112+
<li>Granted by: <%= @granted_by.name %></li>
113+
<% if @extension.comment.present? %>
114+
<li>Comment: <%= @extension.comment %></li>
115+
<% end %>
116+
</ul>
117+
<% end %>
118+
</div>
119+
120+
<div class="footer">
121+
<p>This is an automated message from Doubtfire.</p>
122+
</div>
123+
</body>
124+
</html>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<% if @is_tutor %>
2+
You have granted extensions for the following students:
3+
4+
Extensions granted:
5+
<% @extensions.each do |extension| %>
6+
- <%= extension.project.student.name %>: <%= extension.task.task_definition.name %>
7+
New due date: <%= extension.task.due_date.strftime("%B %d, %Y") %>
8+
<% end %>
9+
10+
Summary:
11+
- Total selected for extension: <%= @total_selected %>
12+
- Successfully granted: <%= @extensions.count %>
13+
<% if @failed_extensions.present? %>
14+
- Failed to grant: <%= @failed_extensions.count %>
15+
16+
Failed extensions:
17+
<% @failed_extensions.each do |failed| %>
18+
- Student ID <%= failed[:student_id] %>: <%= failed[:error] %>
19+
<% end %>
20+
<% end %>
21+
<% else %>
22+
Dear <%= @student.name %>,
23+
24+
An extension has been granted for your task: <%= @task.task_definition.name %>
25+
26+
Details:
27+
- New due date: <%= @task.due_date.strftime("%B %d, %Y") %>
28+
- Granted by: <%= @granted_by.name %>
29+
<% if @extension.comment.present? %>
30+
- Comment: <%= @extension.comment %>
31+
<% end %>
32+
<% end %>
33+
34+
Cheers,
35+
The <%= @doubtfire_product_name %> Team
36+
37+
---
38+
To unsubscribe from these notifications, visit: <%= @unsubscribe_url %>

config/environments/test.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
# The :test delivery method accumulates sent emails in the
2828
# ActionMailer::Base.deliveries array.
2929
config.action_mailer.delivery_method = :test
30+
config.action_mailer.perform_deliveries = true
31+
config.action_mailer.raise_delivery_errors = true
32+
config.action_mailer.default_url_options = { host: 'test.host' }
3033

3134
# Print deprecation notices to the stderr
3235
config.active_support.deprecation = :stderr

0 commit comments

Comments
 (0)