Merge pull request #6469 from aignatov-bio/ai-sci-9512-implement-acitvity-notifications

Implement acitvity notifications [SCI-9512]
This commit is contained in:
Martin Artnik 2023-10-18 09:28:30 +02:00 committed by GitHub
commit 3bf218b5f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 255 additions and 173 deletions

View file

@ -94,7 +94,7 @@
border-radius: 50%;
color: $color-white;
display: flex;
grid-row: 1 / 4;
grid-row: 1 / 5;
height: 2rem;
justify-content: center;
margin-right: .75rem;

View file

@ -2,13 +2,10 @@
class UserNotificationsController < ApplicationController
def index
page = (params[:page] || 1).to_i
notifications = load_notifications.page(page).per(Constants::INFINITE_SCROLL_LIMIT).without_count
page = (params.dig(:page, :number) || 1).to_i
notifications = load_notifications.page(page).per(Constants::INFINITE_SCROLL_LIMIT)
render json: {
notifications: notification_serializer(notifications),
next_page: notifications.next_page
}
render json: notifications, each_serializer: NotificationSerializer
notifications.mark_as_read!
end
@ -26,17 +23,4 @@ class UserNotificationsController < ApplicationController
.order(created_at: :desc)
end
def notification_serializer(notifications)
notifications.map do |notification|
{
id: notification.id,
type_of: notification.type,
title: notification.to_notification.title,
message: notification.to_notification.message,
created_at: I18n.l(notification.created_at, format: :full),
today: notification.created_at.today?,
checked: notification.read_at.present?
}
end
end
end

View file

@ -1,15 +1,23 @@
<template>
<div class="sci-navigation--notificaitons-flyout-notification">
<div class="sci-navigation--notificaitons-flyout-notification-icon" :class="notification.type_of">
<div class="sci-navigation--notificaitons-flyout-notification-icon" :class="notification.attributes.type_of">
<i :class="icon"></i>
</div>
<div class="sci-navigation--notificaitons-flyout-notification-date">
{{ notification.created_at }}
{{ notification.attributes.created_at }}
</div>
<div class="sci-navigation--notificaitons-flyout-notification-title"
v-html="notification.title"
:data-seen="notification.checked"></div>
<div v-html="notification.message" class="sci-navigation--notificaitons-flyout-notification-message"></div>
v-html="notification.attributes.title"
:data-seen="notification.attributes.checked"></div>
<div v-html="notification.attributes.message" class="sci-navigation--notificaitons-flyout-notification-message"></div>
<div v-if="notification.attributes.breadcrumbs" class="flex items-center flex-wrap gap-0.5">
<template v-for="(breadcrumb, index) in notification.attributes.breadcrumbs">
<div :key="index" class="flex items-center gap-0.5">
<i v-if="index > 0" class="sn-icon sn-icon-right"></i>
<a :href="breadcrumb.url" :title="breadcrumb.name" class="truncate max-w-[20ch] inline-block">{{ breadcrumb.name }}</a>
</div>
</template>
</div>
</div>
</template>
@ -21,7 +29,7 @@ export default {
},
computed: {
icon() {
switch(this.notification.type_of) {
switch(this.notification.attributes.type_of) {
case 'deliver':
return 'fas fa-truck';
case 'assignment':

View file

@ -24,6 +24,7 @@
<script>
import NotificationItem from './notification_item.vue'
import axios from '../../../packs/custom_axios.js';
export default {
name: 'NotificationsFlyout',
@ -37,12 +38,13 @@ export default {
data() {
return {
notifications: [],
nextPage: 1,
nextPageUrl: null,
scrollBar: null,
loadingPage: false
}
},
created() {
this.nextPageUrl = this.notificationsUrl;
this.loadNotifications();
},
mounted() {
@ -58,23 +60,29 @@ export default {
this.loadNotifications();
},
todayNotifications() {
return this.notifications.filter(n => n.today);
return this.notifications.filter(n => n.attributes.today);
},
olderNotifications() {
return this.notifications.filter(n => !n.today);
return this.notifications.filter(n => !n.attributes.today);
}
},
methods: {
loadNotifications() {
if (this.nextPage == null || this.loadingPage) return;
if (this.nextPageUrl == null || this.loadingPage) return;
this.loadingPage = true;
$.getJSON(this.notificationsUrl, { page: this.nextPage }, (result) => {
this.notifications = this.notifications.concat(result.notifications);
this.nextPage = result.next_page;
this.loadingPage = false;
this.$emit('update:unseenNotificationsCount');
});
axios.get(this.nextPageUrl)
.then(response => {
this.notifications = this.notifications.concat(response.data.data);
this.nextPageUrl = response.data.links.next;
this.loadingPage = false;
})
.catch(error => {
this.loadingPage = false;
});
}
}
}

View file

@ -9,100 +9,14 @@ module GenerateNotificationModel
end
def generate_notification_from_activity
return if notification_recipients.none?
message = generate_activity_content(self, no_links: true, no_custom_links: true)
description = generate_notification_description_elements(subject).reverse.join(' | ')
notification_recipients.each do |user|
ActivityNotification.with(
title: sanitize_input(message),
message: sanitize_input(description)
).deliver_later(user)
end
params = { activity_id: id, type: "#{type_of}_activity".to_sym }
ActivityNotification.send_notifications(params, later: true)
end
protected
def notification_recipients
users = []
case subject
when Project
users = subject.users
when Experiment
users = subject.users
when MyModule
users = subject.designated_users
# Also send to the user that was unassigned,
# and is therefore no longer present on the module.
if type_of == 'undesignate_user_from_my_module'
users += User.where(id: values.dig('message_items', 'user_target', 'id'))
end
when Protocol
users = subject.in_repository? ? [] : subject.my_module.designated_users
when Result
users = subject.my_module.designated_users
when Repository
users = subject.team.users
when Team
users = subject.users
when Report
users = subject.team.users
when ProjectFolder
users = subject.team.users
end
users - [owner]
end
# This method returns unsanitized elements. They must be sanitized before saving to DB
def generate_notification_description_elements(object, elements = [])
case object
when Project
path = Rails.application.routes.url_helpers.project_path(object)
elements << "#{I18n.t('search.index.project')} <a href='#{path}'>#{object.name}</a>"
when Experiment
path = Rails.application.routes.url_helpers.my_modules_experiment_path(object)
elements << "#{I18n.t('search.index.experiment')} <a href='#{path}'>#{object.name}</a>"
generate_notification_description_elements(object.project, elements)
when MyModule
path = if object.archived?
Rails.application.routes.url_helpers.my_modules_experiment_path(object.experiment, view_mode: :archived)
else
Rails.application.routes.url_helpers.protocols_my_module_path(object)
end
elements << "#{I18n.t('search.index.module')} <a href='#{path}'>#{object.name}</a>"
generate_notification_description_elements(object.experiment, elements)
when Protocol
if object.in_repository?
path = Rails.application.routes.url_helpers.protocols_path(team: object.team.id)
elements << "#{I18n.t('search.index.protocol')} <a href='#{path}'>#{object.name}</a>"
generate_notification_description_elements(object.team, elements)
else
generate_notification_description_elements(object.my_module, elements)
end
when Result
generate_notification_description_elements(object.my_module, elements)
when Repository
path = Rails.application.routes.url_helpers.repository_path(object, team: object.team.id)
elements << "#{I18n.t('search.index.repository')} <a href='#{path}'>#{object.name}</a>"
generate_notification_description_elements(object.team, elements)
when Team
path = Rails.application.routes.url_helpers.projects_path(team: object.id)
elements << "#{I18n.t('search.index.team')} <a href='#{path}'>#{object.name}</a>"
when Report
path = Rails.application.routes.url_helpers.reports_path(team: object.team.id)
elements << "#{I18n.t('search.index.report')} <a href='#{path}'>#{object.name}</a>"
generate_notification_description_elements(object.team, elements)
when ProjectFolder
generate_notification_description_elements(object.team, elements)
end
elements
end
def notifiable?
type_of.in? ::Extends::NOTIFIABLE_ACTIVITIES
NotificationExtends::NOTIFICATIONS_TYPES.key?("#{type_of}_activity".to_sym)
end
private
@ -110,14 +24,4 @@ module GenerateNotificationModel
def generate_notification
CreateNotificationFromActivityJob.perform_later(self) if notifiable?
end
def notification_type
return :recent_changes unless instance_of?(Activity)
if type_of.in? Activity::ASSIGNMENT_TYPES
:assignment
else
:recent_changes
end
end
end

View file

@ -1,22 +0,0 @@
# frozen_string_literal: true
class UserNotification < ApplicationRecord
include NotificationsHelper
belongs_to :user, optional: true
belongs_to :notification, optional: true
after_create :send_email
def self.unseen_notification_count(user)
where('user_id = ? AND checked = false', user.id).count
end
def self.seen_by_user(user)
where(user: user).where(checked: false).update_all(checked: true)
end
def send_email
send_email_notification(user, notification) if user.enabled_notifications_for?(notification.type_of.to_sym, :email)
end
end

View file

@ -5,10 +5,16 @@
# ActivityNotification.with(post: @post).deliver_later(current_user)
# ActivityNotification.with(post: @post).deliver(current_user)
class ActivityNotification < Noticed::Base
class ActivityNotification < BaseNotification
include SearchHelper
include GlobalActivitiesHelper
include InputSanitizeHelper
include ActionView::Helpers::TextHelper
include ApplicationHelper
include ActiveRecord::Sanitization::ClassMethods
include Rails.application.routes.url_helpers
# Add your delivery methods
#
deliver_by :database
# deliver_by :email, mailer: "UserMailer"
# deliver_by :slack
# deliver_by :custom, class: "MyDeliveryMethod"
@ -18,21 +24,35 @@ class ActivityNotification < Noticed::Base
# param :post
def message
# if params[:legacy]
params[:message]
#else
# new logic
# end
params[:message] if params[:legacy]
end
def title
# if params[:legacy]
params[:title]
# else
# new logic
# end
if params[:legacy]
params[:title]
else
generate_activity_content(activity)
end
end
def subject
activity.subject unless params[:legacy]
end
# def url
# post_path(params[:post])
# end
private
def current_team
@current_team ||= recipient.teams.find_by(id: recipient.current_team_id)
end
def current_user
recipient
end
def activity
@activity ||= Activity.find_by(id: params[:activity_id])
end
end

View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
class BaseNotification < Noticed::Base
deliver_by :database, if: :database_notification?
def self.send_notifications(params, later: false)
recipients_class = "Recipients::#{NotificationExtends::NOTIFICATIONS_TYPES[params[:type]][:recipients_module]}".constantize
recipients_class.new(params).recipients.each do |recipient|
if later
with(params).deliver_later(recipient)
else
with(params).deliver(recipient)
end
end
end
def subject; end
private
def database_notification?
recipient.notifications_settings.dig(notification_subgroup.to_s, 'in_app')
end
def notification_subgroup
NotificationExtends::NOTIFICATIONS_GROUPS.values.reduce({}, :merge)
.find { |_sg, n| n.include?(params[:type].to_sym) }[0]
end
end

View file

@ -5,10 +5,9 @@
# DeliveryNotification.with(post: @post).deliver_later(current_user)
# DeliveryNotification.with(post: @post).deliver(current_user)
class DeliveryNotification < Noticed::Base
class DeliveryNotification < BaseNotification
# Add your delivery methods
#
deliver_by :database
# deliver_by :email, mailer: "UserMailer"
# deliver_by :slack
# deliver_by :custom, class: "MyDeliveryMethod"

View file

@ -5,10 +5,9 @@
# GeneralNotification.with(post: @post).deliver_later(current_user)
# GeneralNotification.with(post: @post).deliver(current_user)
class GeneralNotification < Noticed::Base
class GeneralNotification < BaseNotification
# Add your delivery methods
#
deliver_by :database
# deliver_by :email, mailer: "UserMailer"
# deliver_by :slack
# deliver_by :custom, class: "MyDeliveryMethod"

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
class Recipients::AssignedGroupRecipients
def initialize(params)
@params = params
end
def recipients
activity = Activity.find(@params[:activity_id])
project = activity.subject
project.team.users.where.not(id: project.user_assignments.where(assigned: 'manually').select(:user_id))
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
class Recipients::AssignedRecipients
def initialize(params)
@params = params
end
def recipients
activity = Activity.find(@params[:activity_id])
User.where(id: activity.values.dig('message_items', 'user_target', 'id'))
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class Recipients::ItemCreatorRecipients
def initialize(params)
@params = params
end
def recipients
[]
end
end

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
class Recipients::MyModuleDesignatedRecipients
def initialize(params)
@params = params
end
def recipients
if @params[:activity_id]
activity_recipients
else
# other recipients
end
end
private
def activity_recipients
activity = Activity.find(@params[:activity_id])
case activity.subject_type
when 'MyModule'
users = activity.subject.designated_users
when 'Protocol', 'Result'
users = activity.subject.my_module.designated_users
when 'Step'
users = activity.subject.protocol.my_module.designated_users
end
users.where.not(id: activity.owner_id)
end
end

View file

@ -0,0 +1,82 @@
# frozen_string_literal: true
class NotificationSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :id, :title, :message, :created_at, :read_at, :type, :breadcrumbs, :checked, :today
def title
object.to_notification.title
end
def breadcrumbs
subject = object.to_notification.subject
generate_breadcrumbs(subject, []) if subject
end
def message
object.to_notification.message
end
def created_at
I18n.l(object.created_at, format: :full)
end
def today
object.created_at.today?
end
def checked
object.read_at.present?
end
private
def generate_breadcrumbs(subject, breadcrumbs)
case subject
when Project
parent = subject.team
url = project_path(subject)
when Experiment
parent = subject.project
url = experiment_path(subject)
when MyModule
parent = subject.experiment
url = protocols_my_module_path(subject)
when Protocol
if subject.in_repository?
parent = subject.team
url = protocol_path(subject)
else
parent = subject.my_module
url = protocols_my_module_path(subject.my_module)
end
when Result
parent = subject.my_module
url = my_module_results_path(subject.my_module)
when ProjectFolder
parent = subject.team
url = project_folder_path(subject)
when RepositoryBase
parent = subject.team
url = repository_path(subject)
when RepositoryRow
parent = subject.team
url = repository_path(subject.repository)
when LabelTemplate
parent = subject.team
url = label_template_path(subject)
when Team
parent = nil
url = projects_path
end
breadcrumbs << { name: subject.name, url: url } if subject.name.present?
if parent
generate_breadcrumbs(parent, breadcrumbs)
else
breadcrumbs.reverse
end
end
end

View file

@ -1,6 +1,10 @@
# frozen_string_literal: true
class MigrateNotificationToNoticed < ActiveRecord::Migration[7.0]
class UserNotification < ApplicationRecord
belongs_to :notification
end
def up
add_column :notifications, :params, :jsonb, default: {}, null: false
add_column :notifications, :type, :string