mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-11-18 23:21:30 +08:00
Merge pull request #6469 from aignatov-bio/ai-sci-9512-implement-acitvity-notifications
Implement acitvity notifications [SCI-9512]
This commit is contained in:
commit
3bf218b5f6
16 changed files with 255 additions and 173 deletions
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
29
app/notifications/base_notification.rb
Normal file
29
app/notifications/base_notification.rb
Normal 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
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
13
app/notifications/recipients/assigned_group_recipients.rb
Normal file
13
app/notifications/recipients/assigned_group_recipients.rb
Normal 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
|
12
app/notifications/recipients/assigned_recipients.rb
Normal file
12
app/notifications/recipients/assigned_recipients.rb
Normal 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
|
11
app/notifications/recipients/item_creator_recipients.rb
Normal file
11
app/notifications/recipients/item_creator_recipients.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Recipients::ItemCreatorRecipients
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def recipients
|
||||
[]
|
||||
end
|
||||
end
|
|
@ -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
|
82
app/serializers/notification_serializer.rb
Normal file
82
app/serializers/notification_serializer.rb
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue