Skip to content

Instantly share code, notes, and snippets.

@leastbad
Forked from fractaledmind/_notification.html.erb
Created February 13, 2021 08:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leastbad/5ccc8daa468701fab89abb21370807dc to your computer and use it in GitHub Desktop.
Save leastbad/5ccc8daa468701fab89abb21370807dc to your computer and use it in GitHub Desktop.
<div class="card">
<div class="card-header">
<h3 class="card-title">
<%= notification.to_notification.in_app_subject %>
</h3>
</div>
<div class="card-body">
<%= notification.to_notification.in_app_body %>
</div>
</div>
internal_payload = {
model_app_name: model.app.name,
model_title: model.title,
model_url: Router.gig_url(model_id),
recipient_full_name: Current.user.full_name
}
model_payload = model.notification_payload || {}
# ensure that our internal payload overrides any values under the same key from the gig notification payload
params = gig_payload.merge(internal_payload)
Notifications::Channel
.deliver_later(via: :new_internal_event,
to: model.user,
with: params)
class Notification < ApplicationRecord
include Noticed::Model
belongs_to :recipient, polymorphic: true
def to_notification
Notifications::Channel.as_notifier(via: type, with: params)
end
end
class NotificationMailer < ApplicationMailer
# This method is needed to allow the `method_missing` to be called
def self.action_methods
all_inclusive = Class.new do
def include?(_)
true
end
end
all_inclusive.new
end
def method_missing(method_name, *_args)
return super unless method_name.to_s.end_with?('_notifier')
# parms == {:recipient=>#<User>, :record=>#<Notification> || nil }
notification = if params[:record]
params[:record].to_notification
else
Notifications::Channel.as_notifier(via: method_name, with: params)
end
mail(to: params[:recipient].email,
subject: notification.email_subject) do |format|
format.html { render html: notification.email_html_body.html_safe }
format.text { render plain: notification.email_text_body }
end
end
def respond_to_missing?(method_name, include_private = false)
super
end
end
class Notifications::Broadcast < ApplicationRecord
belongs_to :channel, class_name: 'Notifications::Channel'
belongs_to :worker_filter
serialize :payload, JSON
validates :payload, presence: true
validate :not_sent_to_internal_channel
after_create_commit :deliver_notification
def recipients
service = WorkerDocumentService.new(channel.app)
worker_documents = service.find_by_worker_filter(worker_filter) # rubocop:disable Rails/DynamicFindBy
all_ids = worker_documents.map { |document| document['app_worker_id'] }
User.joins(:worker).merge(Worker.joins(:app_workers)).where(app_workers: { id: all_ids })
end
private
def deliver_notification
data = payload.is_a?(String) ? JSON.parse(payload) : payload
channel.deliver(to: recipients, with: data)
end
def not_sent_to_internal_channel
return if channel_id.nil?
return unless channel.internal?
errors.add(:channel, :broadcast_to_internal_channel)
end
end
class Notifications::Channel < ApplicationRecord
belongs_to :app
has_many :templates, class_name: 'Notifications::Template', dependent: :destroy
has_many :broadcasts, class_name: 'Notifications::Broadcast', dependent: :destroy
has_many :user_preferences, class_name: 'Notifications::Preference', dependent: :destroy
has_one :in_app_template, -> { in_app.order(updated_at: :desc) }, class_name: 'Notifications::Template', inverse_of: :channel
has_one :email_html_template, -> { email.html.order(updated_at: :desc) }, class_name: 'Notifications::Template', inverse_of: :channel
has_one :email_text_template, -> { email.text.order(updated_at: :desc) }, class_name: 'Notifications::Template', inverse_of: :channel
validates :name, presence: true, uniqueness: { scope: :app_id, case_sensitive: true }
scope :internal, -> { where(internal: true) }
def self.deliver(via:, to:, with:)
as_notifier(via: via, with: with)
&.deliver(to)
end
def self.deliver_later(via:, to:, with:)
as_notifier(via: via, with: with)
&.deliver_later(to)
end
def self.as_notifier(via:, with:)
channel_name = via.to_s.remove('_notifier').remove('Notifier')
find_by_name(channel_name) # rubocop:disable Rails/DynamicFindBy
&.as_notifier(with: with)
end
def self.find_by_name(channel_name)
find_by(name: [
channel_name,
channel_name.to_s.underscore,
channel_name.to_s.camelize
])
end
def deliver(to:, with:)
as_notifier(with: with)
.deliver(to)
end
def deliver_later(to:, with:)
as_notifier(with: with)
.deliver_later(to)
end
def as_notifier(with:)
to_noticed_class
.with(with)
end
private
def to_noticed_class
# we need local variables for the `define_method` closures to have access to these data
in_app_subject_template = in_app_template&.subject
in_app_body_template = in_app_template&.body
email_subject_template = (email_html_template || email_text_template)&.subject
email_html_body_template = email_html_template&.body
email_text_body_template = email_text_template&.body
channel_id = id
klass = Class.new(Noticed::Base) do
deliver_by :database, if: :in_app_notifications?
deliver_by :email, mailer: 'NotificationMailer', if: :email_notifications?
define_method :in_app_notifications? do
preference = preferences&.in_app?
return preference unless preference.nil?
true
end
define_method :email_notifications? do
preference = preferences&.email?
return preference unless preference.nil?
true
end
define_method :in_app_subject do
render_template(in_app_subject_template)
end
define_method :in_app_body do
render_template(in_app_body_template).html_safe
end
define_method :email_subject do
render_template(email_subject_template)
end
define_method :email_html_body do
render_template(email_html_body_template)
end
define_method :email_text_body do
render_template(email_text_body_template)
end
define_method :preferences do
recipient.notification_preferences.find_by(channel_id: channel_id)
end
define_method :render_template do |template|
return '' unless template
template % attributes
end
define_method :attributes do
original = params.symbolize_keys
Hash.new { |_, k| original[k] || '' }
end
end
# to avoid ever seeing something like #<#<Class:0x00007fd58318d340>:0x00007fd5868c5088> in logs or the console
Object.const_set "#{app.name.camelize}#{name.camelize}Notifier", klass
end
end
class NotificationsController < Workers::BaseController
def index
@notifications = current_user.notifications.unread.newest_first.load
@notifications.mark_as_read!
end
end
class Notifications::Preference < ApplicationRecord
belongs_to :channel, class_name: 'Notifications::Channel'
belongs_to :user
end
class Notifications::Template < ApplicationRecord
belongs_to :channel, class_name: 'Notifications::Channel'
enum deliver_by: { email: 'email', in_app: 'in_app' }, _prefix: false
enum format: { text: 'text', html: 'html' }, _prefix: false
validates :subject, presence: true
validates :body, presence: true
validates :deliver_by, presence: true
validates :format, presence: true
validates :channel, uniqueness: { scope: [:format, :deliver_by] }
# I find this semantically more readable than the default `deliver_by_in_app` generated by the `enum`
scope :deliver_in_app, -> { where(deliver_by: :in_app) }
end
ActiveRecord::Schema.define(version: 2021_02_12_094634) do
create_table "notifications", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
t.string "recipient_type", null: false
t.bigint "recipient_id", null: false
t.string "type", null: false
t.json "payload"
t.datetime "read_at"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["read_at"], name: "index_notifications_on_read_at"
t.index ["recipient_type", "recipient_id"], name: "index_notifications_on_recipient_type_and_recipient_id"
end
create_table "notifications_broadcasts", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
t.bigint "channel_id", null: false
t.text "params"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.bigint "worker_filter_id", null: false
t.index ["channel_id"], name: "index_notifications_broadcasts_on_channel_id"
t.index ["worker_filter_id"], name: "index_notifications_broadcasts_on_worker_filter_id"
end
create_table "notifications_channels", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
t.string "name"
t.bigint "app_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.boolean "internal", default: false
t.index ["app_id"], name: "index_notifications_channels_on_app_id"
end
create_table "notifications_preferences", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
t.bigint "channel_id", null: false
t.bigint "user_id", null: false
t.boolean "in_app"
t.boolean "email"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["channel_id"], name: "index_notifications_preferences_on_channel_id"
t.index ["user_id"], name: "index_notifications_preferences_on_user_id"
end
create_table "notifications_templates", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
t.string "subject"
t.text "body"
t.string "deliver_by"
t.string "format"
t.bigint "channel_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["channel_id"], name: "index_notifications_templates_on_channel_id"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment