Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/api/api_root.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class ApiRoot < Grape::API
mount UnitContentsApi
mount UnitsApi
mount TutorNotesApi
mount NotificationsApi

mount D2lIntegrationApi::D2lApi
mount D2lIntegrationApi::OauthPublicApi
Expand Down Expand Up @@ -162,6 +163,7 @@ class ApiRoot < Grape::API
AuthenticationHelpers.add_auth_to DiscussionPromptsApi
AuthenticationHelpers.add_auth_to OverseerStepsApi
AuthenticationHelpers.add_auth_to TutorNotesApi
AuthenticationHelpers.add_auth_to NotificationsApi

add_swagger_documentation \
base_path: nil,
Expand Down
170 changes: 170 additions & 0 deletions app/api/notifications_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# frozen_string_literal: true

require 'grape'

class NotificationsApi < Grape::API
helpers AuthenticationHelpers

before do
authenticated?
end

helpers do
def notification_scope
current_user
.received_notifications
.includes(:recipient, :unit, task: [:task_definition, { project: :user }])
end

def unread_group_count
current_user.received_notifications.unread.pluck(:task_id, :unit_id, :kind, :metadata).map do |task_id, unit_id, kind, metadata|
if task_id.present?
"task:#{task_id}"
elsif kind == 'tutor_note'
"tutor-notes:#{unit_id}:#{metadata['unit_role_id']}"
else
"unit:#{unit_id}:#{kind}"
end
end.uniq.count
end

def serialize_preference(preference)
{
id: preference.id,
unit: {
id: preference.unit.id,
code: preference.unit.code,
name: preference.unit.name
},
email_categories: preference.email_categories,
email_frequency: preference.email_frequency,
email_time: preference.email_time,
email_weekday: preference.email_weekday,
timezone: preference.timezone,
next_digest_at: preference.next_digest_at,
last_digest_at: preference.last_digest_at
}
end

def accessible_unit_ids
project_units = current_user.projects.where(enrolled: true).select(:unit_id)
role_units = current_user.unit_roles.select(:unit_id)
preference_units = current_user.notification_preferences.select(:unit_id)

Unit.where(id: project_units).or(Unit.where(id: role_units)).or(Unit.where(id: preference_units)).pluck(:id)

Check warning on line 54 in app/api/notifications_api.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace 'pluck(:id)' with the more semantic 'ids' method.

See more on https://sonarcloud.io/project/issues?id=doubtfire-lms_doubtfire-api&issues=AZ-sgp6NdnmC--q0BX2t&open=AZ-sgp6NdnmC--q0BX2t&pullRequest=661
end
end

desc 'Get grouped notifications for the current user'
params do
optional :state, type: String, values: %w[all unread read], default: 'all'
optional :unit_id, type: Integer
optional :kinds, type: Array[String], values: Notification::KINDS
optional :query, type: String
optional :page, type: Integer, default: 1, values: ->(value) { value.positive? }
optional :per_page, type: Integer, default: 25, values: 1..50
end
get '/notifications' do
scope = notification_scope
scope = scope.where(unit_id: params[:unit_id]) if params[:unit_id]
scope = scope.where(kind: params[:kinds]) if params[:kinds].present?

scope =
case params[:state]
when 'unread'
scope.unread
when 'read'
scope.recently_read
else
scope.where('notifications.read_at IS NULL OR notifications.read_at >= ?', 30.days.ago)
end

groups = NotificationGroupBuilder.new(scope).groups
if params[:query].present?
query = params[:query].downcase
groups.select! do |group|
[
group[:summary],
group.dig(:unit, :code),
group.dig(:unit, :name),
group.dig(:task, :abbreviation),
group.dig(:task, :name),
group.dig(:task, :student_name)
].compact.any? { |value| value.to_s.downcase.include?(query) }
end
end

page = params[:page]
per_page = params[:per_page]
total = groups.count

{
groups: groups.slice((page - 1) * per_page, per_page) || [],
page: page,
per_page: per_page,
total: total,
unread_count: unread_group_count
}
end

desc 'Get the grouped unread notification count for the current user'
get '/notifications/unread_count' do
{ count: unread_group_count }
end

desc 'Mark selected notifications as read'
params do
requires :notification_ids, type: Array[Integer]
end
put '/notifications/read' do
scope = current_user.received_notifications.where(id: params[:notification_ids]).unread
count = scope.count
Notification.mark_read(scope)
{ count: count }
end

desc 'Mark all notifications as read'
params do
optional :unit_id, type: Integer
end
put '/notifications/read_all' do
scope = current_user.received_notifications.unread
scope = scope.where(unit_id: params[:unit_id]) if params[:unit_id]
count = scope.count
Notification.mark_read(scope)
{ count: count }
end

desc 'Get per-unit notification email preferences'
get '/notification_preferences' do
preferences = Unit.where(id: accessible_unit_ids).order(:code).map do |unit|
NotificationPreference.for(current_user, unit)
end

preferences.map { |preference| serialize_preference(preference) }
end

desc 'Update notification email preferences for a unit'
params do
requires :email_categories, type: Array[String], values: Notification::KINDS
requires :email_frequency, type: String, values: NotificationPreference::FREQUENCIES
requires :email_time, type: String
requires :email_weekday, type: Integer
requires :timezone, type: String
end
put '/notification_preferences/:unit_id' do
unit_id = params[:unit_id].to_i
error!({ error: 'You do not have access to notification settings for this unit' }, 403) unless accessible_unit_ids.include?(unit_id)

preference = NotificationPreference.for(current_user, Unit.find(unit_id))
preference.update!(
email_categories: params[:email_categories],
email_frequency: params[:email_frequency],
email_time: params[:email_time],
email_weekday: params[:email_weekday],
timezone: params[:timezone]
)

serialize_preference(preference)
end
end
2 changes: 2 additions & 0 deletions app/api/task_comments_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ class TaskCommentsApi < Grape::API
# mark every comment type except for DiscussionComments so we don't mark it as read.
comments_to_mark_as_read = comments.where("TYPE is null OR TYPE != 'DiscussionComment'")
task.mark_comments_as_read(current_user, comments_to_mark_as_read)
Notification.mark_task_read(current_user, task)
else
result = []
end
Expand Down Expand Up @@ -267,6 +268,7 @@ class TaskCommentsApi < Grape::API

task_comment = task.comments.find(params[:id])
task_comment.mark_as_unread(current_user)
Notification.reopen_for_source(task_comment, current_user)

SessionTracker.record_assessment_activity(
action: 'mark-comment-unread',
Expand Down
20 changes: 20 additions & 0 deletions app/mailers/notifications_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,26 @@ def weekly_student_summary(project, summary_stats, did_revert_to_pass)
mail(to: email_with_name, from: tutor_email, subject: subject)
end

def notification_digest(preference, notifications)
return nil if preference.nil? || notifications.blank?

add_general
@recipient = preference.user
@unit = preference.unit
@groups = NotificationGroupBuilder.new(notifications).groups
@notification_count = notifications.count
@notification_url = "#{@doubtfire_host}/notifications"
@sender = @unit.main_convenor_user
return nil if @sender.nil?

subject = "#{@unit.code}: #{@notification_count} new #{'change'.pluralize(@notification_count)} across #{@groups.count} #{'notification'.pluralize(@groups.count)}"
mail(
to: %("#{@recipient.name}" <#{@recipient.email}>),
from: %("#{@sender.name}" <#{@sender.email}>),
subject: subject
)
end

def discussion_deadline_approaching(task, sender, expiry_date)
add_discussion_deadline_details(task, sender)
@deadline = task.unit.formatted_discuss_timeout_date(expiry_date)
Expand Down
4 changes: 4 additions & 0 deletions app/models/comments/task_comment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class TaskComment < ApplicationRecord
belongs_to :recipient, class_name: 'User', optional: false

has_many :comments_read_receipts, class_name: 'CommentsReadReceipts', dependent: :destroy, inverse_of: :task_comment
has_many :notifications, as: :source, dependent: :destroy

# Can optionally be a reply to a comment
belongs_to :task_comment, optional: true
Expand All @@ -33,6 +34,9 @@ class TaskComment < ApplicationRecord
after_create do
mark_as_read(self.user)
end
after_create_commit do
Notification.create_for_task_comment(self)
end

# Delete action - before dependent association
before_destroy :delete_associated_files
Expand Down
4 changes: 0 additions & 4 deletions app/models/comments/task_status_comment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ class TaskStatusComment < TaskComment
self.content_type = :status
end

after_create do
mark_as_read(self.recipient)
end

def serialize(user)
json = super(user)
json[:recipient_read_time] = nil
Expand Down
Loading
Loading