diff --git a/Gemfile b/Gemfile index e90ed1171..512c38b36 100644 --- a/Gemfile +++ b/Gemfile @@ -56,6 +56,7 @@ gem 'jbuilder' # JSON structures via a Builder-style DSL gem 'logging', '~> 2.0.0' gem 'nested_form_fields' gem 'nokogiri', '~> 1.14.3' # HTML/XML parser +gem 'noticed' gem 'rails_autolink', '~> 1.1', '>= 1.1.6' gem 'rgl' # Graph framework for project diagram calculations gem 'roo', '~> 2.10.0' # Spreadsheet parser diff --git a/Gemfile.lock b/Gemfile.lock index 983c917d8..7d3d534ae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -298,6 +298,8 @@ GEM discard (1.2.1) activerecord (>= 4.2, < 8) docile (1.4.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) doorkeeper (5.6.6) railties (>= 5) down (5.4.1) @@ -321,6 +323,9 @@ GEM faraday-net_http (3.0.2) fastimage (2.2.7) ffi (1.15.5) + ffi-compiler (1.0.1) + ffi (>= 1.0.0) + rake figaro (1.2.0) thor (>= 0.14.0, < 2) fugit (1.8.1) @@ -332,6 +337,14 @@ GEM process-pipeline hashdiff (1.0.1) hashie (5.0.0) + http (5.1.1) + addressable (~> 2.8) + http-cookie (~> 1.0) + http-form_data (~> 2.2) + llhttp-ffi (~> 0.4.0) + http-cookie (1.0.5) + domain_name (~> 0.5) + http-form_data (2.3.0) httparty (0.21.0) mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) @@ -381,6 +394,9 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) little-plugger (1.1.4) + llhttp-ffi (0.4.0) + ffi-compiler (~> 1.0) + rake (~> 13.0) logging (2.0.0) little-plugger (~> 1.1) multi_json (~> 1.10) @@ -428,6 +444,9 @@ GEM racc (~> 1.4) nokogiri (1.14.5-x86_64-linux) racc (~> 1.4) + noticed (1.6.3) + http (>= 4.0.0) + rails (>= 5.2.0) oauth2 (2.0.9) faraday (>= 0.17.3, < 3.0) jwt (>= 1.0, < 3.0) @@ -654,6 +673,9 @@ GEM uglifier (4.2.0) execjs (>= 0.3.0, < 3) underscore-rails (1.8.3) + unf (0.1.4) + unf_ext + unf_ext (0.0.8.2) unicode-display_width (2.4.2) uniform_notifier (1.16.0) version_gem (1.1.3) @@ -740,6 +762,7 @@ DEPENDENCIES nested_form_fields newrelic_rpm nokogiri (~> 1.14.3) + noticed omniauth (~> 2.1) omniauth-azure-activedirectory-v2 omniauth-linkedin-oauth2 diff --git a/app/controllers/user_notifications_controller.rb b/app/controllers/user_notifications_controller.rb index 214ccc033..eb2c7e976 100644 --- a/app/controllers/user_notifications_controller.rb +++ b/app/controllers/user_notifications_controller.rb @@ -10,14 +10,12 @@ class UserNotificationsController < ApplicationController next_page: notifications.next_page } - UserNotification.where( - notification_id: notifications.except(:select).where.not(type_of: 2).select(:id) - ).seen_by_user(current_user) + notifications.mark_as_read! end def unseen_counter render json: { - unseen: load_notifications.where('user_notifications.checked = ?', false).size + unseen: load_notifications.where(read_at: nil).size } end @@ -25,7 +23,6 @@ class UserNotificationsController < ApplicationController def load_notifications current_user.notifications - .select(:id, :type_of, :title, :message, :created_at, 'user_notifications.checked') .order(created_at: :desc) end @@ -33,12 +30,12 @@ class UserNotificationsController < ApplicationController notifications.map do |notification| { id: notification.id, - type_of: notification.type_of, - title: notification.title, - message: notification.message, + 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.checked + checked: notification.read_at.present? } end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1f5d3ae55..6fdc6a3a5 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -101,12 +101,10 @@ module ApplicationHelper end def generate_annotation_notification(target_user, title, message) - notification = Notification.create( - type_of: :assignment, + GeneralNotification.with( title: sanitize_input(title), message: sanitize_input(message) - ) - UserNotification.create(notification: notification, user: target_user) if target_user.assignments_notification + ).deliver_later(target_user) end def custom_link_open_new_tab(text) diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 5cce5d3e6..4635eb814 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -18,14 +18,9 @@ module NotificationsHelper message = "#{I18n.t('search.index.team')} #{team.name}" end - notification = Notification.create( - type_of: :assignment, + GeneralNotification.with( title: sanitize_input(title), message: sanitize_input(message) - ) - - if target_user.assignments_notification - notification.create_user_notification(target_user) - end + ).deliver_later(target_user) end end diff --git a/app/jobs/concerns/failed_delivery_notifiable_job.rb b/app/jobs/concerns/failed_delivery_notifiable_job.rb index ba5458f8b..e3b374333 100644 --- a/app/jobs/concerns/failed_delivery_notifiable_job.rb +++ b/app/jobs/concerns/failed_delivery_notifiable_job.rb @@ -23,12 +23,11 @@ module FailedDeliveryNotifiableJob @user = User.find_by(id: arguments.last[:user_id]) return if @user.blank? - notification = Notification.create!( - type_of: :deliver_error, + DeliveryNotification.with( title: failed_notification_title, - message: failed_notification_message - ) - notification.create_user_notification(@user) + message: failed_notification_message, + error: true + ).deliver_later(@user) end def failed_notification_title diff --git a/app/jobs/protocols/docx_import_job.rb b/app/jobs/protocols/docx_import_job.rb index 0bfb494c4..bfa967ef1 100644 --- a/app/jobs/protocols/docx_import_job.rb +++ b/app/jobs/protocols/docx_import_job.rb @@ -136,15 +136,13 @@ module Protocols "href='#{Rails.application.routes.url_helpers.rails_blob_path(@tmp_files.take.file)}'>" \ "#{@tmp_files.take.file.filename}" - notification = Notification.create!( - type_of: :deliver, + DeliveryNotification.with( title: I18n.t('protocols.import_export.import_protocol_notification.title', link: original_file_download_link), - message: "#{I18n.t('protocols.import_export.import_protocol_notification.message')} " \ - "" \ - "#{@protocol.name}" - ) - notification.create_user_notification(@user) + message: "#{I18n.t('protocols.import_export.import_protocol_notification.message')} " \ + "" \ + "#{@protocol.name}" + ).deliver_later(@user) end # Overrides method from FailedDeliveryNotifiableJob concern diff --git a/app/jobs/reports/docx_job.rb b/app/jobs/reports/docx_job.rb index 6f9cc6d92..ae1d5a743 100644 --- a/app/jobs/reports/docx_job.rb +++ b/app/jobs/reports/docx_job.rb @@ -21,16 +21,15 @@ module Reports report.docx_ready! report_path = Rails.application.routes.url_helpers .reports_path(team: report.team.id, preview_report_id: report.id, preview_type: :docx) - notification = Notification.create( - type_of: :deliver, + + DeliveryNotification.with( title: I18n.t('projects.reports.index.generation.completed_docx_notification_title'), message: I18n.t('projects.reports.index.generation.completed_notification_message', report_link: "#{escape_input(report.name)}", team_name: escape_input(report.team.name)) - ) + ).deliver_later(user) Reports::DocxPreviewJob.perform_now(report.id) - notification.create_user_notification(user) ensure I18n.backend.date_format = nil file.close diff --git a/app/jobs/reports/pdf_job.rb b/app/jobs/reports/pdf_job.rb index 2bce30b56..e1b43527a 100644 --- a/app/jobs/reports/pdf_job.rb +++ b/app/jobs/reports/pdf_job.rb @@ -60,14 +60,13 @@ module Reports report_path = Rails.application.routes.url_helpers .reports_path(team: report.team.id, preview_report_id: report.id, preview_type: :pdf) - notification = Notification.create( - type_of: :deliver, + + DeliveryNotification.with( title: I18n.t('projects.reports.index.generation.completed_pdf_notification_title'), message: I18n.t('projects.reports.index.generation.completed_notification_message', report_link: "#{escape_input(report.name)}", team_name: escape_input(report.team.name)) - ) - notification.create_user_notification(user) + ).deliver_later(user) ensure I18n.backend.date_format = nil file.close(true) diff --git a/app/jobs/repositories_export_job.rb b/app/jobs/repositories_export_job.rb index 911334621..4d93eeab6 100644 --- a/app/jobs/repositories_export_job.rb +++ b/app/jobs/repositories_export_job.rb @@ -83,18 +83,16 @@ class RepositoriesExportJob < ApplicationJob end def generate_notification - notification = Notification.create!( - type_of: :deliver, + DeliveryNotification.with( title: I18n.t('zip_export.notification_title'), - message: "" \ - "#{@zip_export.zip_file_name}" - ) - notification.create_user_notification(@user) + message: "" \ + "#{@zip_export.zip_file_name}" + ).deliver_later(@user) end # Overrides method from FailedDeliveryNotifiableJob concern diff --git a/app/jobs/zip_export_job.rb b/app/jobs/zip_export_job.rb index fbbb2e47c..8620a0fbd 100644 --- a/app/jobs/zip_export_job.rb +++ b/app/jobs/zip_export_job.rb @@ -34,17 +34,15 @@ class ZipExportJob < ApplicationJob end def generate_notification! - notification = Notification.create!( - type_of: :deliver, + DeliveryNotification.with( title: I18n.t('zip_export.notification_title'), - message: "" \ "#{@zip_export.zip_file_name}" - ) - notification.create_user_notification(@user) + ).deliver_later(@user) end end diff --git a/app/models/concerns/generate_notification_model.rb b/app/models/concerns/generate_notification_model.rb index deb443a06..ed8c1335b 100644 --- a/app/models/concerns/generate_notification_model.rb +++ b/app/models/concerns/generate_notification_model.rb @@ -14,15 +14,11 @@ module GenerateNotificationModel message = generate_activity_content(self, no_links: true, no_custom_links: true) description = generate_notification_description_elements(subject).reverse.join(' | ') - notification = Notification.create( - type_of: notification_type, - title: sanitize_input(message), - message: sanitize_input(description), - generator_user_id: owner.id - ) - notification_recipients.each do |user| - notification.create_user_notification(user) + ActivityNotification.with( + title: sanitize_input(message), + message: sanitize_input(description) + ).deliver_later(user) end end diff --git a/app/models/notification.rb b/app/models/notification.rb index dd6829ec2..d0a4f3919 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -5,16 +5,11 @@ class Notification < ApplicationRecord has_many :users, through: :user_notifications belongs_to :generator_user, class_name: 'User', optional: true + include Noticed::Model + belongs_to :recipient, polymorphic: true + enum type_of: Extends::NOTIFICATIONS_TYPES - def create_user_notification(user) - return if user == generator_user - return unless can_send_to_user?(user) - return unless user.enabled_notifications_for?(type_of.to_sym, :web) - - user_notifications.create!(user: user) - end - private def can_send_to_user?(_user) diff --git a/app/models/user.rb b/app/models/user.rb index d659da492..0ee836291 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -307,8 +307,7 @@ class User < ApplicationRecord inverse_of: :created_by, dependent: :destroy - has_many :user_notifications, inverse_of: :user - has_many :notifications, through: :user_notifications + has_many :notifications, as: :recipient, dependent: :destroy, inverse_of: :recipient has_many :zip_exports, inverse_of: :user, dependent: :destroy has_many :view_states, dependent: :destroy diff --git a/app/notifications/activity_notification.rb b/app/notifications/activity_notification.rb new file mode 100644 index 000000000..1b6ec34e8 --- /dev/null +++ b/app/notifications/activity_notification.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# To deliver this notification: +# +# ActivityNotification.with(post: @post).deliver_later(current_user) +# ActivityNotification.with(post: @post).deliver(current_user) + +class ActivityNotification < Noticed::Base + # Add your delivery methods + # + deliver_by :database + # deliver_by :email, mailer: "UserMailer" + # deliver_by :slack + # deliver_by :custom, class: "MyDeliveryMethod" + + # Add required params + # + # param :post + + def message + # if params[:legacy] + params[:message] + #else + # new logic + # end + end + + def title + # if params[:legacy] + params[:title] + # else + # new logic + # end + end + # def url + # post_path(params[:post]) + # end +end diff --git a/app/notifications/delivery_notification.rb b/app/notifications/delivery_notification.rb new file mode 100644 index 000000000..a161df661 --- /dev/null +++ b/app/notifications/delivery_notification.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# To deliver this notification: +# +# DeliveryNotification.with(post: @post).deliver_later(current_user) +# DeliveryNotification.with(post: @post).deliver(current_user) + +class DeliveryNotification < Noticed::Base + # Add your delivery methods + # + deliver_by :database + # deliver_by :email, mailer: "UserMailer" + # deliver_by :slack + # deliver_by :custom, class: "MyDeliveryMethod" + + # Add required params + # + # param :post + + # Define helper methods to make rendering easier. + # + def message + # if params[:legacy] + params[:message] + # else + # new logic + # end + end + + def title + # if params[:legacy] + params[:title] + # else + # new logic + # end + end +end diff --git a/app/notifications/general_notification.rb b/app/notifications/general_notification.rb new file mode 100644 index 000000000..375224df1 --- /dev/null +++ b/app/notifications/general_notification.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# To deliver this notification: +# +# GeneralNotification.with(post: @post).deliver_later(current_user) +# GeneralNotification.with(post: @post).deliver(current_user) + +class GeneralNotification < Noticed::Base + # Add your delivery methods + # + deliver_by :database + # deliver_by :email, mailer: "UserMailer" + # deliver_by :slack + # deliver_by :custom, class: "MyDeliveryMethod" + + # Add required params + # + # param :post + + # Define helper methods to make rendering easier. + # + def message + # if params[:legacy] + params[:message] + # else + # new logic + # end + end + + def title + # if params[:legacy] + params[:title] + # else + # new logic + # end + end + # + # def url + # post_path(params[:post]) + # end +end diff --git a/db/migrate/20231011103114_migrate_notification_to_noticed.rb b/db/migrate/20231011103114_migrate_notification_to_noticed.rb new file mode 100644 index 000000000..e460ee6e6 --- /dev/null +++ b/db/migrate/20231011103114_migrate_notification_to_noticed.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +class MigrateNotificationToNoticed < ActiveRecord::Migration[7.0] + def up + add_column :notifications, :params, :jsonb, default: {}, null: false + add_column :notifications, :type, :string, null: false, default: 'LegacyNotification' + add_column :notifications, :read_at, :datetime + add_column :notifications, :recipient_id, :bigint + add_column :notifications, :recipient_type, :string + + type_mapping = { + 'assignment' => 'ActivityNotification', + 'recent_changes' => 'GeneralNotification', + 'deliver' => 'DeliveryNotification', + 'deliver_error' => 'DeliveryNotification' + } + + UserNotification.all.each do |user_notification| + notification = user_notification.notification.dup + + new_type = type_mapping[notification.type_of] + + params = { + title: notification.title, + message: notification.message, + legacy: true + } + + params[:error] = notification.type_of == 'deliver_error' if new_type == 'DeliveryNotification' + notification.update( + params: params, + type: new_type, + read_at: (user_notification.updated_at if user_notification.checked), + recipient_id: user_notification.user_id, + recipient_type: 'User', + created_at: user_notification.created_at, + updated_at: user_notification.updated_at + ) + end + + Notification.where(type: 'LegacyNotification').destroy_all + + remove_column :notifications, :type_of + remove_column :notifications, :title + remove_column :notifications, :message + remove_column :notifications, :generator_user_id + end +end diff --git a/db/schema.rb b/db/schema.rb index be3be339f..b0af8dfd6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_09_04_080206) do +ActiveRecord::Schema[7.0].define(version: 2023_10_11_103114) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gist" enable_extension "pg_trgm" @@ -377,12 +377,13 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_04_080206) do end create_table "notifications", force: :cascade do |t| - t.string "title" - t.string "message" - t.integer "type_of", null: false - t.bigint "generator_user_id" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false + t.jsonb "params", default: {}, null: false + t.string "type", default: "LegacyNotification", null: false + t.datetime "read_at" + t.bigint "recipient_id" + t.string "recipient_type" t.index ["created_at"], name: "index_notifications_on_created_at" end @@ -1347,7 +1348,6 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_04_080206) do add_foreign_key "my_modules", "users", column: "created_by_id" add_foreign_key "my_modules", "users", column: "last_modified_by_id" add_foreign_key "my_modules", "users", column: "restored_by_id" - add_foreign_key "notifications", "users", column: "generator_user_id" add_foreign_key "oauth_access_grants", "oauth_applications", column: "application_id" add_foreign_key "oauth_access_grants", "users", column: "resource_owner_id" add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id"