Class: Effective::Notification

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/effective/notification.rb

Constant Summary collapse

AUDIENCES =
[
  ['Send to user or email from the report', 'report'],
  ['Send to specific addresses', 'emails']
]
SCHEDULE_TYPES =
[
  ['On the first day they appear in the report and every x days thereafter', 'immediate'],
  ['When present in the report on the following dates', 'scheduled']
]
SCHEDULED_METHODS =

TODO: [‘Send once’, ‘Send daily’, ‘Send weekly’, ‘Send monthly’, ‘Send quarterly’, ‘Send yearly’, ‘Send now’]

[
  ['The following dates...', 'dates'],
]
CONTENT_TYPES =
['text/plain', 'text/html']

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#current_resourceObject

Returns the value of attribute current_resource.



8
9
10
# File 'app/models/effective/notification.rb', line 8

def current_resource
  @current_resource
end

#current_userObject

Returns the value of attribute current_user.



7
8
9
# File 'app/models/effective/notification.rb', line 7

def current_user
  @current_user
end

#view_contextObject

Returns the value of attribute view_context.



9
10
11
# File 'app/models/effective/notification.rb', line 9

def view_context
  @view_context
end

Instance Method Details

#already_notified_today?(resource) ⇒ Boolean



349
350
351
352
353
354
355
356
357
358
# File 'app/models/effective/notification.rb', line 349

def already_notified_today?(resource)
  email = resource_email(resource) || resource_user(resource).try(:email)
  raise("expected an email for #{report} #{report&.id} and #{resource} #{resource&.id}") unless email.present?

  logs = notification_logs.select { |log| log.email == email }
  return false if logs.count == 0

  # If we already notified today
  logs.any? { |log| log.created_at&.beginning_of_day == Time.zone.now.beginning_of_day }
end

#assign_renderer(view_context) ⇒ Object



210
211
212
213
214
# File 'app/models/effective/notification.rb', line 210

def assign_renderer(view_context)
  raise('expected renderer to respond to') unless view_context.respond_to?(:root_url)
  assign_attributes(view_context: view_context)
  self
end

#assigns_for(resource = nil) ⇒ Object

We pull the Assigns from 3 places:

  1. The report.report_columns

  2. The class’s def reportable_view_assigns(view) method



421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# File 'app/models/effective/notification.rb', line 421

def assigns_for(resource = nil)
  return {} unless report.present? 

  resource ||= report.reportable.new
  raise('expected an acts_as_reportable resource') unless resource.class.try(:acts_as_reportable?)
  
  report_assigns = Array(report.report_columns).inject({}) do |h, column|
    value = resource.send(column.name)
    h[column.name] = column.format(value); h
  end

  reportable_view_assigns = resource.reportable_view_assigns(renderer).deep_stringify_keys
  raise('expected notification assigns to return a Hash') unless reportable_view_assigns.kind_of?(Hash)

  # Merge all 3
  report_assigns.merge(reportable_view_assigns)
end

#audience_emailsObject



190
191
192
# File 'app/models/effective/notification.rb', line 190

def audience_emails
  Array(self[:audience_emails]) - [nil, '']
end

#audience_emails?Boolean



176
177
178
# File 'app/models/effective/notification.rb', line 176

def audience_emails?
  audience == 'emails'
end

#audience_report?Boolean



180
181
182
# File 'app/models/effective/notification.rb', line 180

def audience_report?
  audience == 'report'
end

#build_notification_log(resource: nil, skipped: false) ⇒ Object



439
440
441
442
443
444
445
446
# File 'app/models/effective/notification.rb', line 439

def build_notification_log(resource: nil, skipped: false)
  user = resource_user(resource)

  email = resource_email(resource) || user.try(:email)
  email ||= audience_emails_to_s if scheduled_email?

  notification_logs.build(email: email, report: report, resource: resource, user: user, skipped: skipped)
end

#disable!Object



236
237
238
# File 'app/models/effective/notification.rb', line 236

def disable!
  update!(enabled: false)
end

#enable!Object



232
233
234
# File 'app/models/effective/notification.rb', line 232

def enable!
  update!(enabled: true)
end

#immediate?Boolean

This operates on each row of the resource. We track the number of notifications total to see if we should notify again or not



168
169
170
# File 'app/models/effective/notification.rb', line 168

def immediate?
  schedule_type == 'immediate'
end

#notifiable?(resource, date: nil) ⇒ Boolean



332
333
334
335
336
337
338
339
340
341
342
# File 'app/models/effective/notification.rb', line 332

def notifiable?(resource, date: nil)
  raise('expected an acts_as_reportable resource') unless resource.class.try(:acts_as_reportable?)

  if schedule_type == 'immediate'
    notifiable_immediate?(resource: resource, date: date)
  elsif schedule_type == 'scheduled'
    notifiable_scheduled?(date: date)
  else
    raise("unsupported schedule_type")
  end
end

#notifiable_immediate?(resource:, date: nil) ⇒ Boolean

Consider the notification logs which track how many and how long ago this notification was sent It’s notifiable? when first time or if it’s been immediate_days since last notification



362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'app/models/effective/notification.rb', line 362

def notifiable_immediate?(resource:, date: nil)
  raise('expected an immediate? notification') unless immediate?

  email = resource_email(resource) || resource_user(resource).try(:email)
  raise("expected an email for #{report} #{report&.id} and #{resource} #{resource&.id}") unless email.present?

  logs = notification_logs.select { |log| log.email == email }

  if logs.count == 0
    true # This is the first time. We should send.
  elsif logs.count < immediate_times
    # We still have to send it but consider dates.
    last_sent_days_ago = logs.map { |log| log.days_ago(date: date) }.min || 0
    (last_sent_days_ago >= immediate_days)
  else
    false # We've already sent enough times
  end
end

#notifiable_rows_countObject



224
225
226
# File 'app/models/effective/notification.rb', line 224

def notifiable_rows_count
  report.collection().select { |resource| notifiable?(resource) }.count if report
end

#notifiable_scheduled?(date: nil) ⇒ Boolean



381
382
383
384
385
386
387
388
389
390
391
392
# File 'app/models/effective/notification.rb', line 381

def notifiable_scheduled?(date: nil)
  raise('expected a scheduled? notification') unless scheduled?

  date ||= Time.zone.now.beginning_of_day

  case scheduled_method
  when 'dates'
    scheduled_dates.find { |day| day == date.strftime('%F') }.present?
  else
    raise('unsupported scheduled_method')
  end
end

#notifiable_tomorrow?(resource) ⇒ Boolean



344
345
346
347
# File 'app/models/effective/notification.rb', line 344

def notifiable_tomorrow?(resource)
  date = Time.zone.now.beginning_of_day.advance(days: 1)
  notifiable?(resource, date: date)
end

#notifiable_tomorrow_rows_countObject



228
229
230
# File 'app/models/effective/notification.rb', line 228

def notifiable_tomorrow_rows_count
  report.collection().select { |resource| notifiable_tomorrow?(resource) }.count if report
end

#notify!(force: false) ⇒ Object

The main function to send this thing



271
272
273
# File 'app/models/effective/notification.rb', line 271

def notify!(force: false)
  scheduled_email? ? notify_by_schedule!(force: force) : notify_by_resources!(force: force)
end

#notify_by_resources!(force: false) ⇒ Object

Operates on every resource in the data source. Sends one email for each row



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'app/models/effective/notification.rb', line 276

def notify_by_resources!(force: false)
  notified = 0

  report.collection().find_each do |resource|
    next unless notifiable?(resource) || force

    # Send Now functionality. Don't duplicate if it's same day.
    next if force && already_notified_today?(resource)

    print('.')

    begin
      # For logging
      assign_attributes(current_resource: resource)

      # Send the resource email
      Effective::NotificationsMailer.notify_resource(self, resource).deliver_now

      # Log that it was sent
      build_notification_log(resource: resource).save!

      # Count how many we actually sent
      notified += 1
    rescue => e
      EffectiveLogger.error(e.message, associated: self) if defined?(EffectiveLogger)
      ExceptionNotifier.notify_exception(e, data: { notification_id: id, resource_id: resource.id, resource_type: resource.class.name }) if defined?(ExceptionNotifier)
    end

    GC.start if (notified % 250) == 0
  end

  notified > 0 ? update!(last_notified_at: Time.zone.now, last_notified_count: notified) : touch
end

#notify_by_schedule!(force: false) ⇒ Object



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'app/models/effective/notification.rb', line 310

def notify_by_schedule!(force: false)
  notified = 0

  if notifiable_scheduled? || force
    begin
      Effective::NotificationsMailer.notify(self).deliver_now

      # Log that it was sent
      build_notification_log(resource: nil).save!

      # Count how many we actually sent
      notified += 1
    rescue => e
      EffectiveLogger.error(e.message, associated: self) if defined?(EffectiveLogger)
      ExceptionNotifier.notify_exception(e, data: { notification_id: id }) if defined?(ExceptionNotifier)
    end

  end

  notified > 0 ? update!(last_notified_at: Time.zone.now, last_notified_count: notified) : touch
end

#render_email(resource = nil) ⇒ Object



394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'app/models/effective/notification.rb', line 394

def render_email(resource = nil)
  raise('expected an acts_as_reportable resource') if resource.present? && !resource.class.try(:acts_as_reportable?)

  to = if audience == 'emails'
    audience_emails.presence
  elsif audience == 'report'
    resource_email(resource) || resource_user(resource).try(:email)
  end

  raise('expected a to email address') unless to.present?

  assigns = assigns_for(resource)

  {
    to: to,
    from: from,
    cc: cc.presence,
    bcc: bcc.presence,
    content_type: CONTENT_TYPES.first,
    subject: template_subject.render(assigns),
    body: template_body.render(assigns)
  }.compact
end

#rendererObject



216
217
218
# File 'app/models/effective/notification.rb', line 216

def renderer
  view_context || nil # This isn't ideal
end

#report_variablesObject



206
207
208
# File 'app/models/effective/notification.rb', line 206

def report_variables
  assigns_for().keys
end

#rows_countObject



220
221
222
# File 'app/models/effective/notification.rb', line 220

def rows_count
  @rows_count ||= report.collection().count if report
end

#scheduleObject



156
157
158
159
160
161
162
163
164
# File 'app/models/effective/notification.rb', line 156

def schedule
  if immediate?
    "Send immediately then every #{immediate_days} days for #{immediate_times} times total"
  elsif scheduled? && scheduled_method == 'dates'
    "Send on #{scheduled_dates.length} scheduled days: #{scheduled_dates.sort.to_sentence}"
  else
    'todo'
  end
end

#scheduled?Boolean



172
173
174
# File 'app/models/effective/notification.rb', line 172

def scheduled?
  schedule_type == 'scheduled'
end

#scheduled_datesObject



194
195
196
# File 'app/models/effective/notification.rb', line 194

def scheduled_dates
  Array(self[:scheduled_dates]) - [nil, '']
end

#scheduled_email?Boolean

Only scheduled emails can have attached reports. Only scheduled emails can do Send Now



186
187
188
# File 'app/models/effective/notification.rb', line 186

def scheduled_email?
  scheduled? && audience_emails?
end

#send_now!Object

Enqueues this notification to send right away. Only applies to scheduled_email? notifications



242
243
244
245
246
# File 'app/models/effective/notification.rb', line 242

def send_now!
  raise('expected to be persisted') unless persisted?
  NotificationJob.perform_later(id, force: true)
  true
end

#skip_once!Object

Only applies to immedate? notifications Skips over one notification on the immediate notifications



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'app/models/effective/notification.rb', line 250

def skip_once!
  notified = 0

  report.collection().find_each do |resource|
    print('.')

    # For logging
    assign_attributes(current_resource: resource)

    # Send the resource email
    build_notification_log(resource: resource, skipped: true).save!

    notified += 1

    GC.start if (notified % 250) == 0
  end

  touch
end

#template_bodyObject



202
203
204
# File 'app/models/effective/notification.rb', line 202

def template_body
  Liquid::Template.parse(body)
end

#template_subjectObject



198
199
200
# File 'app/models/effective/notification.rb', line 198

def template_subject
  Liquid::Template.parse(subject)
end

#to_sObject



152
153
154
# File 'app/models/effective/notification.rb', line 152

def to_s
  subject.presence || model_name.human
end