effective_messaging 0.12.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -0
  3. data/app/controllers/admin/chat_messages_controller.rb +0 -1
  4. data/app/controllers/admin/chats_controller.rb +0 -1
  5. data/app/controllers/admin/notification_logs_controller.rb +0 -1
  6. data/app/controllers/admin/notifications_controller.rb +0 -1
  7. data/app/controllers/effective/chats_controller.rb +12 -2
  8. data/app/datatables/admin/effective_chat_messages_datatable.rb +0 -3
  9. data/app/datatables/admin/effective_chats_datatable.rb +0 -2
  10. data/app/datatables/effective_chats_datatable.rb +0 -1
  11. data/app/jobs/effective/notification_job.rb +0 -2
  12. data/app/mailers/effective/messaging_mailer.rb +0 -1
  13. data/app/mailers/effective/notifications_mailer.rb +0 -1
  14. data/app/models/concerns/effective_messaging_parent.rb +0 -1
  15. data/app/models/concerns/effective_messaging_user.rb +0 -1
  16. data/app/models/effective/chat.rb +2 -4
  17. data/app/models/effective/chat_message.rb +8 -1
  18. data/app/models/effective/chat_user.rb +1 -2
  19. data/app/models/effective/notification.rb +76 -6
  20. data/app/models/effective/notification_log.rb +0 -1
  21. data/app/views/admin/chats/_form.html.haml +1 -1
  22. data/app/views/admin/notifications/_form.html.haml +1 -1
  23. data/app/views/admin/notifications/_notification.html.haml +1 -1
  24. data/app/views/effective/chats/_chat_message.html.haml +3 -0
  25. data/app/views/effective/chats/_form.html.haml +2 -0
  26. data/config/effective_messaging.rb +4 -2
  27. data/config/routes.rb +3 -2
  28. data/db/migrate/101_create_effective_messaging.rb +0 -1
  29. data/lib/effective_messaging/engine.rb +0 -1
  30. data/lib/effective_messaging/version.rb +1 -1
  31. data/lib/effective_messaging.rb +1 -3
  32. data/lib/generators/effective_messaging/install_generator.rb +0 -1
  33. metadata +3 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7122fe35acd896e24f84b7fd65ba9514fb9a1dff0de6e50f0ac5af6d507f643b
4
- data.tar.gz: a997df0342bbd6fa50f0ec0e87911f8518d00e5538d2f69f3686bba3c1e15923
3
+ metadata.gz: b067851235561d03267380eeaf0e8c4f734c635e7c28689c0cc67830a4a84b6f
4
+ data.tar.gz: 9d21a71ec1dbc58dd46a174342c8eadd37eb0fb7c7042d1ed8106b785ba4a4fd
5
5
  SHA512:
6
- metadata.gz: 84bd8c6a88b84dccd311c582a52648d1b2c0763e7575ef558274dd94d28ba5c56e4cae26eaa9e23cb0527e5c33af6035e691ced6d6184cae9ef35ff4b6228050
7
- data.tar.gz: d6091da483e055c52b6f5fb60fa4b76b31759e986d90af906b8a43624129bc4067ccb2fc533fef2c4aaa34411588c9d2fe3b0f83fb9778d24e6788350d9df17a
6
+ metadata.gz: 596dedbf28e9c3fc0cfa5f0d592f61d309992833a71c7c483e5ba3488dd7094626d77a7ce77833bd96203f116dfdd61c3eeceb160b28624cd7eeed184d777d53
7
+ data.tar.gz: faccebd0025d5fe3b2815d69d6ddafbec4a4aa5a7177b945c4c21decd9fa92b63192d220840f91194f22688e240e913ad0fa302048ce496ac6bd6c982f63ccea
data/README.md CHANGED
@@ -4,6 +4,15 @@ Centralize all communications between one or more users.
4
4
 
5
5
  Works with effective_reports to schedule notifications to report results containing emails.
6
6
 
7
+ `notification.delivery_status` provides a read-only forecast using the same `notifiable?`
8
+ checks as `notifiable_tomorrow_rows_count` and the sender. It returns `:next_on`, `:status`,
9
+ the last recorded send date, and counts of current recipients pending, finished, or blocked.
10
+ Resource notifications also respect same-day delivery logs and delivery errors. Skipped
11
+ logs count toward a course, as they do when sending. A completed course applies only to
12
+ the current report recipients; new matches may still receive notifications. Fixed-address
13
+ scheduled emails follow their configured dates even when the report is empty. This method
14
+ does not send email, write logs, or predict changes to the report audience or server cron.
15
+
7
16
  ## Getting Started
8
17
 
9
18
  This requires Rails 6+ and Twitter Bootstrap 4 and just works with Devise.
@@ -142,3 +151,14 @@ rails test
142
151
  4. Push to the branch (`git push origin my-new-feature`)
143
152
  5. Bonus points for test coverage
144
153
  6. Create new Pull Request
154
+
155
+ ## Chat documents
156
+
157
+ Documents are disabled by default. Enable them in the host initializer:
158
+
159
+ ```ruby
160
+ config.chat_attachments_enabled = true
161
+ ```
162
+
163
+ Messages still require text. Documents remain with their message and downloads
164
+ require permission to view the chat. Disabling the toggle hides documents and blocks downloads without deleting files. The host needs Active Storage configured and migrated.
@@ -8,6 +8,5 @@ module Admin
8
8
  def permitted_params
9
9
  params.require(:effective_chat_message).permit!
10
10
  end
11
-
12
11
  end
13
12
  end
@@ -8,6 +8,5 @@ module Admin
8
8
  def permitted_params
9
9
  params.require(:effective_chat).permit!
10
10
  end
11
-
12
11
  end
13
12
  end
@@ -10,6 +10,5 @@ module Admin
10
10
  def permitted_params
11
11
  params.require(:effective_notification_log).permit!
12
12
  end
13
-
14
13
  end
15
14
  end
@@ -10,6 +10,5 @@ module Admin
10
10
  def permitted_params
11
11
  params.require(:effective_notification).permit!
12
12
  end
13
-
14
13
  end
15
14
  end
@@ -6,11 +6,21 @@ module Effective
6
6
 
7
7
  resource_scope -> { Effective::Chat.all.deep }
8
8
 
9
+ def download
10
+ raise ActiveRecord::RecordNotFound unless EffectiveMessaging.chat_attachments_enabled
11
+ chat = Effective::Chat.find(params[:id])
12
+ EffectiveResources.authorize!(self, :show, chat)
13
+ attachment = chat.chat_messages.find(params[:chat_message_id]).files.find(params[:file_id])
14
+ send_data attachment.download, filename: attachment.filename.to_s,
15
+ type: attachment.content_type, disposition: 'attachment'
16
+ end
17
+
9
18
  private
10
19
 
11
20
  def permitted_params
12
- params.require(:effective_chat).except(:user_type).permit(chat_messages_attributes: [:body])
21
+ message_attributes = [:body]
22
+ message_attributes << { files: [] } if EffectiveMessaging.chat_attachments_enabled
23
+ params.require(:effective_chat).except(:user_type).permit(chat_messages_attributes: message_attributes)
13
24
  end
14
-
15
25
  end
16
26
  end
@@ -1,6 +1,5 @@
1
1
  module Admin
2
2
  class EffectiveChatMessagesDatatable < Effective::Datatable
3
-
4
3
  datatable do
5
4
  col :id, visible: false
6
5
  col :token, visible: false
@@ -23,8 +22,6 @@ module Admin
23
22
  end
24
23
 
25
24
  scope
26
-
27
25
  end
28
-
29
26
  end
30
27
  end
@@ -1,6 +1,5 @@
1
1
  module Admin
2
2
  class EffectiveChatsDatatable < Effective::Datatable
3
-
4
3
  datatable do
5
4
  col :id, visible: false
6
5
  col :token, visible: false
@@ -46,6 +45,5 @@ module Admin
46
45
 
47
46
  scope
48
47
  end
49
-
50
48
  end
51
49
  end
@@ -23,5 +23,4 @@ class EffectiveChatsDatatable < Effective::Datatable
23
23
 
24
24
  chats
25
25
  end
26
-
27
26
  end
@@ -1,9 +1,7 @@
1
1
  module Effective
2
2
  class NotificationJob < ApplicationJob
3
-
4
3
  def perform(id, force:)
5
4
  Notification.find(id).notify!(force: force)
6
5
  end
7
-
8
6
  end
9
7
  end
@@ -53,6 +53,5 @@ module Effective
53
53
 
54
54
  { user: values }
55
55
  end
56
-
57
56
  end
58
57
  end
@@ -76,6 +76,5 @@ module Effective
76
76
  def authorize!(action, resource)
77
77
  true
78
78
  end
79
-
80
79
  end
81
80
  end
@@ -39,5 +39,4 @@ module EffectiveMessagingParent
39
39
  def chat_url(chat:, user:, root_url:)
40
40
  nil
41
41
  end
42
-
43
42
  end
@@ -43,5 +43,4 @@ module EffectiveMessagingUser
43
43
  # def build_chat_user(chat:)
44
44
  # chat_user(chat: chat) || chat_users.build(chat: chat)
45
45
  # end
46
-
47
46
  end
@@ -34,8 +34,8 @@ module Effective
34
34
  scope :sorted, -> { order(id: :desc) }
35
35
  scope :deep, -> { includes(:chat_users, :chat_messages) }
36
36
 
37
- scope :for_user, -> (user) { where(id: ChatUser.where(user: user).select(:chat_id)) }
38
- scope :search_by_chat_user_name, -> (name) { where(id: ChatUser.with_name(name).select(:chat_id)) }
37
+ scope :for_user, ->(user) { where(id: ChatUser.where(user: user).select(:chat_id)) }
38
+ scope :search_by_chat_user_name, ->(name) { where(id: ChatUser.with_name(name).select(:chat_id)) }
39
39
 
40
40
  validates :title, presence: true, length: { maximum: 255 }
41
41
 
@@ -65,7 +65,6 @@ module Effective
65
65
 
66
66
  # Notify everyone in the chat except the user that created this message
67
67
  notified = (chat_users - [except]).map do |chat_user|
68
-
69
68
  # Only notify once every 5 minutes unless force
70
69
  if chat_user.last_notified_at.present? && !force
71
70
  next if (Time.zone.now - chat_user.last_notified_at) < NOTIFY_AFTER
@@ -130,6 +129,5 @@ module Effective
130
129
  # Return users
131
130
  users
132
131
  end
133
-
134
132
  end
135
133
  end
@@ -6,6 +6,14 @@ module Effective
6
6
 
7
7
  log_changes(to: :chat) if respond_to?(:log_changes)
8
8
 
9
+ has_many_attached :files
10
+
11
+ validate do
12
+ if attachment_changes['files'].present? && !EffectiveMessaging.chat_attachments_enabled
13
+ errors.add(:files, 'are disabled for chats')
14
+ end
15
+ end
16
+
9
17
  belongs_to :chat, counter_cache: true
10
18
 
11
19
  # Who sent this message
@@ -43,6 +51,5 @@ module Effective
43
51
  def truncate(value)
44
52
  value.length > 100 ? (value.first(100) + '...') : value
45
53
  end
46
-
47
54
  end
48
55
  end
@@ -9,7 +9,7 @@ module Effective
9
9
  belongs_to :chat
10
10
  belongs_to :user, polymorphic: true
11
11
 
12
- scope :with_name, -> (name) {
12
+ scope :with_name, ->(name) {
13
13
  anonymous = where(chat_id: Chat.where(anonymous: true)).where('anonymous_name ILIKE ?', "%#{name}%")
14
14
  displayed = where(chat_id: Chat.where(anonymous: false)).where('display_name ILIKE ?', "%#{name}%")
15
15
 
@@ -43,6 +43,5 @@ module Effective
43
43
  def name
44
44
  chat&.anonymous? ? anonymous_name : display_name
45
45
  end
46
-
47
46
  end
48
47
  end
@@ -38,7 +38,7 @@ module Effective
38
38
 
39
39
  # TODO: ['Send once', 'Send daily', 'Send weekly', 'Send monthly', 'Send quarterly', 'Send yearly', 'Send now']
40
40
  SCHEDULED_METHODS = [
41
- ['The following dates...', 'dates'],
41
+ ['The following dates...', 'dates']
42
42
  ]
43
43
 
44
44
  CONTENT_TYPES = ['text/plain', 'text/html']
@@ -208,6 +208,65 @@ module Effective
208
208
  report.collection().select { |resource| notifiable_tomorrow?(resource) }.count if report
209
209
  end
210
210
 
211
+ # Read-only forecast using the same eligibility checks as the sender.
212
+ # A completed immediate course applies to the current report recipients only.
213
+ def delivery_status(date: Time.zone.today)
214
+ return { status: :disabled, next_on: nil } unless enabled?
215
+
216
+ date = date.to_date
217
+ logs = notification_logs.to_a
218
+ result = { next_on: nil, last_sent_on: logs.reject(&:skipped?).map { |log| log.created_at&.to_date }.compact.max }
219
+ dates = nil
220
+
221
+ if scheduled?
222
+ dates = scheduled_dates.map { |value| Date.iso8601(value) rescue nil }.compact.uniq.sort
223
+ .select { |day| day >= date && notifiable_scheduled?(date: day) }
224
+ result[:next_scheduled_on] = dates.first
225
+ return result.merge(status: :finished) if dates.empty?
226
+
227
+ # These send once to the configured addresses, regardless of report membership.
228
+ # The sender does not deduplicate repeated task runs on the same scheduled day.
229
+ if scheduled_email?
230
+ status = dates.first == date ? :due : :scheduled
231
+ result[:sent_today] = result[:last_sent_on] == date
232
+ return result.merge(status: status, next_on: dates.first)
233
+ end
234
+ end
235
+
236
+ recipients = report.collection.find_each.group_by { |resource| resource_emails_to_s(resource) }
237
+ missing = recipients.delete(nil).to_a.size
238
+ logs_by_email = logs.group_by(&:email)
239
+ pending_dates = []
240
+ completed = blocked = 0
241
+
242
+ recipients.each do |email, resources|
243
+ # Duplicate email keys share a course; prefer a row without delivery errors.
244
+ resource = resources.find { |row| !email_delivery_error?(row) } || resources.first
245
+ next_on = next_delivery_on(resource, date: date, logs: logs_by_email.fetch(email, []), dates: dates)
246
+ if next_on.nil?
247
+ completed += 1
248
+ elsif email_delivery_error?(resource)
249
+ blocked += 1
250
+ else
251
+ pending_dates << next_on
252
+ end
253
+ end
254
+
255
+ next_on = pending_dates.min
256
+ status = if next_on
257
+ next_on == date ? :due : :scheduled
258
+ elsif blocked > 0 || missing > 0
259
+ :blocked
260
+ elsif completed > 0
261
+ :complete
262
+ else
263
+ :waiting
264
+ end
265
+
266
+ result.merge(status: status, next_on: next_on, pending_count: pending_dates.length,
267
+ completed_count: completed, blocked_count: blocked, missing_email_count: missing)
268
+ end
269
+
211
270
  def enable!
212
271
  update!(enabled: true)
213
272
  end
@@ -327,11 +386,12 @@ module Effective
327
386
  notified > 0 ? update!(last_notified_at: Time.zone.now, last_notified_count: notified) : touch
328
387
  end
329
388
 
330
- def notifiable?(resource, date: nil)
389
+ # Forecasts can supply logs already filtered to the resource's email key.
390
+ def notifiable?(resource, date: nil, logs: nil)
331
391
  raise('expected an acts_as_reportable resource') unless resource.class.try(:acts_as_reportable?)
332
392
 
333
393
  if schedule_type == 'immediate'
334
- notifiable_immediate?(resource: resource, date: date)
394
+ notifiable_immediate?(resource: resource, date: date, logs: logs)
335
395
  elsif schedule_type == 'scheduled'
336
396
  notifiable_scheduled?(date: date)
337
397
  else
@@ -357,13 +417,13 @@ module Effective
357
417
 
358
418
  # Consider the notification logs which track how many and how long ago this notification was sent
359
419
  # It's notifiable? when first time or if it's been immediate_days since last notification
360
- def notifiable_immediate?(resource:, date: nil)
420
+ def notifiable_immediate?(resource:, date: nil, logs: nil)
361
421
  raise('expected an immediate? notification') unless immediate?
362
422
 
363
423
  email = resource_emails_to_s(resource)
364
424
  return false if email.blank?
365
425
 
366
- logs = notification_logs.select { |log| log.email == email }
426
+ logs ||= notification_logs.select { |log| log.email == email }
367
427
 
368
428
  if logs.count == 0
369
429
  true # This is the first time. We should send.
@@ -425,6 +485,17 @@ module Effective
425
485
 
426
486
  private
427
487
 
488
+ def next_delivery_on(resource, date:, logs:, dates:)
489
+ unless dates
490
+ last_on = logs.map { |log| log.created_at&.to_date || date }.max
491
+ dates = [last_on ? [date, last_on + immediate_days].max : date]
492
+ end
493
+
494
+ dates.find do |day|
495
+ notifiable?(resource, date: day, logs: logs) && logs.none? { |log| log.created_at&.to_date == day }
496
+ end
497
+ end
498
+
428
499
  def audience_emails_to_s
429
500
  audience_emails.presence&.join(', ')
430
501
  end
@@ -466,6 +537,5 @@ module Effective
466
537
 
467
538
  resource.public_send(column.name)
468
539
  end
469
-
470
540
  end
471
541
  end
@@ -30,6 +30,5 @@ module Effective
30
30
  now = (date || Time.zone.now).to_date
31
31
  (now - (created_at&.to_date || now)).to_i
32
32
  end
33
-
34
33
  end
35
34
  end
@@ -7,5 +7,5 @@
7
7
  = render_inline_datatable(Admin::EffectiveChatMessagesDatatable.new(chat: chat))
8
8
 
9
9
  - if chat.respond_to?(:logs_datatable)
10
- = tab "Logs" do
10
+ = tab "Logs", lazy: true do
11
11
  = render_inline_datatable(chat.logs_datatable)
@@ -7,5 +7,5 @@
7
7
  - datatable = Admin::EffectiveNotificationLogsDatatable.new(notification: notification)
8
8
  = render_inline_datatable(datatable)
9
9
 
10
- = tab 'Logs' do
10
+ = tab 'Logs', lazy: true do
11
11
  = render_inline_datatable(notification.logs_datatable)
@@ -7,5 +7,5 @@
7
7
  - datatable = Admin::EffectiveNotificationLogsDatatable.new(notification: notification)
8
8
  = render_inline_datatable(datatable)
9
9
 
10
- = tab 'Logs' do
10
+ = tab 'Logs', lazy: true do
11
11
  = render_inline_datatable(notification.logs_datatable)
@@ -7,3 +7,6 @@
7
7
  .col.text-right= chat_message.created_at.strftime("%b %e, %Y %k:%M")
8
8
  .card-body
9
9
  = simple_format(chat_message.body)
10
+ - if EffectiveMessaging.chat_attachments_enabled
11
+ - chat_message.files.each do |file|
12
+ %p= link_to file.filename.to_s, effective_messaging.download_chat_path(chat_message.chat, chat_message_id: chat_message.id, file_id: file.id)
@@ -14,5 +14,7 @@
14
14
  = f.fields_for :chat_messages, chat_message do |fcm|
15
15
  - # The user and name are set by controller's current_user
16
16
  = fcm.text_area :body, required: true, label: 'Message'
17
+ - if EffectiveMessaging.chat_attachments_enabled
18
+ = fcm.file_field :files, label: 'Documents'
17
19
 
18
20
  = f.submit et('effective_messaging.send'), border: false
@@ -1,10 +1,13 @@
1
1
  EffectiveMessaging.setup do |config|
2
+ # Allow documents on chat messages. Disabled unless explicitly enabled.
3
+ config.chat_attachments_enabled = false
4
+
2
5
  # Layout Settings
3
6
  # Configure the Layout per controller, or all at once
4
7
  # config.layout = { application: 'application', admin: 'admin' }
5
8
 
6
9
  # Mailer Settings
7
- # Please see config/initializers/effective_messaging.rb for default effective_* gem mailer settings
10
+ # Please see config/initializers/effective_resources.rb for default effective_* gem mailer settings
8
11
  #
9
12
  # Configure the class responsible to send e-mails.
10
13
  # config.mailer = 'Effective::MessagingMailer'
@@ -18,5 +21,4 @@ EffectiveMessaging.setup do |config|
18
21
  # config.mailer_froms = nil # Default Froms collection
19
22
  # config.mailer_admin = nil # Default To value for Admin correspondence
20
23
  # config.mailer_subject = nil # Proc.new method used to customize Subject
21
-
22
24
  end
data/config/routes.rb CHANGED
@@ -7,7 +7,9 @@ end
7
7
  EffectiveMessaging::Engine.routes.draw do
8
8
  # Public routes
9
9
  scope module: 'effective' do
10
- resources :chats, only: [:show, :update]
10
+ resources :chats, only: [:show, :update] do
11
+ get :download, on: :member
12
+ end
11
13
  end
12
14
 
13
15
  namespace :admin do
@@ -24,5 +26,4 @@ EffectiveMessaging::Engine.routes.draw do
24
26
 
25
27
  resources :notification_logs, only: [:index, :destroy]
26
28
  end
27
-
28
29
  end
@@ -92,6 +92,5 @@ class CreateEffectiveMessaging < ActiveRecord::Migration[6.0]
92
92
 
93
93
  t.timestamps
94
94
  end
95
-
96
95
  end
97
96
  end
@@ -14,6 +14,5 @@ module EffectiveMessaging
14
14
  ActiveRecord::Base.extend(EffectiveMessagingUser::Base)
15
15
  end
16
16
  end
17
-
18
17
  end
19
18
  end
@@ -1,3 +1,3 @@
1
1
  module EffectiveMessaging
2
- VERSION = '0.12.3'.freeze
2
+ VERSION = '0.13.0'.freeze
3
3
  end
@@ -4,11 +4,10 @@ require 'effective_messaging/engine'
4
4
  require 'effective_messaging/version'
5
5
 
6
6
  module EffectiveMessaging
7
-
8
7
  def self.config_keys
9
8
  [
10
9
  :chats_table_name, :chat_users_table_name, :chat_messages_table_name, :notifications_table_name, :notification_logs_table_name,
11
- :layout,
10
+ :layout, :chat_attachments_enabled,
12
11
  :mailer, :parent_mailer, :deliver_method, :mailer_layout, :mailer_sender, :mailer_froms, :mailer_admin, :mailer_subject
13
12
  ]
14
13
  end
@@ -22,5 +21,4 @@ module EffectiveMessaging
22
21
  def self.Notification
23
22
  Effective::Notification
24
23
  end
25
-
26
24
  end
@@ -22,7 +22,6 @@ module EffectiveMessaging
22
22
  def create_migration_file
23
23
  migration_template ('../' * 3) + 'db/migrate/101_create_effective_messaging.rb', 'db/migrate/create_effective_messaging.rb'
24
24
  end
25
-
26
25
  end
27
26
  end
28
27
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: effective_messaging
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.3
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code and Effect
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-06-03 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -276,7 +275,6 @@ homepage: https://github.com/code-and-effect/effective_messaging
276
275
  licenses:
277
276
  - MIT
278
277
  metadata: {}
279
- post_install_message:
280
278
  rdoc_options: []
281
279
  require_paths:
282
280
  - lib
@@ -291,8 +289,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
291
289
  - !ruby/object:Gem::Version
292
290
  version: '0'
293
291
  requirements: []
294
- rubygems_version: 3.5.9
295
- signing_key:
292
+ rubygems_version: 4.0.17
296
293
  specification_version: 4
297
294
  summary: Centralize the communication between one or more users.
298
295
  test_files: []