notey 0.2.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a4f1b1236c0e43b64f5624a251fdc64f4ce94cd3e12f000efb4619799d6c7c4
4
- data.tar.gz: 8bc6db30919be535ab9b1a6064f8ba58be63791b0cb091fe63a6dca47e57df76
3
+ metadata.gz: 7610a3ba662e8faccd42819f0422ba19dd6b23f65619de246b28b89b201d7cbd
4
+ data.tar.gz: ce7e9b0e188efd5790e8b2d9a8545d0fdfc418133db9f7fca3f78cf529eb2e71
5
5
  SHA512:
6
- metadata.gz: 3e29900e2827d55ab8efd6bebe99a5a73fdd4d49b709dbeeafeefc73aeddbf31fba242105b0dd7e547f15852855e5a3e45dd3bf4df51fc6365dc156da936a094
7
- data.tar.gz: cb4ae5408587d29aab57d7e87f94a5fbfcd3097eb93a2e5a0e9335816f704560c42acde42f6c1bd3b970ab043cb9163249a8d4672b59e04a26d4a9244ae414bf
6
+ metadata.gz: 961ec39375572d803830b55c13bc140c44a2a999d7ce6bdc55ee5124ef5bf9f059adb46bbb87fce89af72b6a9bfb61ce5617bdd513fb0be725d48df2cbf88e4e
7
+ data.tar.gz: cf7398efa6cd3d2a7f8afe4ff6a7dd83076db6cc4929f3ca80506c590d5ab86aadf5fea04ec6cb3cac8119d4d00ec34c7e3112dc2c25ecc4e8dde29f44b44125
data/README.md CHANGED
@@ -163,8 +163,8 @@ end
163
163
 
164
164
  ## Settings sections
165
165
 
166
- Notey ships its two pages as partials and their saving as action objects, so a
167
- settings shell renders them inside its own chrome rather than linking away.
166
+ Notey ships its screens as partials and their saving as action objects, so a
167
+ settings shell renders them inside its own page frame rather than linking away.
168
168
  With [`bureau`](https://github.com/DYB-Development/bureau):
169
169
 
170
170
  ```ruby
@@ -180,6 +180,29 @@ A section registered this way is served by the settings shell, so the shell's
180
180
  own capability check guards it. A section that only links to a mounted path is
181
181
  not guarded, because the shell never renders it.
182
182
 
183
+ ## The inbox
184
+
185
+ The inbox is the same shape and is not a setting, so render it wherever a
186
+ person's notifications belong in your own pages:
187
+
188
+ ```erb
189
+ <%= render "notey/inbox",
190
+ person: current_user,
191
+ account: current_account.id,
192
+ submit_url: notifications_path %>
193
+ ```
194
+
195
+ It lists the notifications that person received in that account, newest first,
196
+ marking the unread ones. Marking one read posts to the url you named, so the
197
+ person stays on your page, and the action behind it runs:
198
+
199
+ ```ruby
200
+ Notey::MarkRead.new(person: current_user, account: current_account.id, values: params).call
201
+ ```
202
+
203
+ It marks only a notification that person received in that account, so an id
204
+ from anywhere else does nothing.
205
+
183
206
  `Notey::SavePreferences` and `Notey::SaveDestination` take `person:`, `account:`
184
207
  and `values:`, and answer with an object responding to `ok?` and `message`. The
185
208
  engine's own pages call the same two actions.
@@ -198,6 +221,26 @@ Each run enqueues one job per person, so one failing send does not stop the
198
221
  rest. A window is sent once even if the run overlaps itself; a send that fails
199
222
  releases the window so it can be sent again.
200
223
 
224
+ ## Deleting old delivery records
225
+
226
+ Notey writes one record per notification per channel that leaves the
227
+ application, and nothing deletes them on its own. Say how long to keep them:
228
+
229
+ ```ruby
230
+ # config/initializers/notey.rb
231
+ Notey.attempt_retention = 90.days
232
+ ```
233
+
234
+ Then run the deletion on a schedule, the way you run the digest windows:
235
+
236
+ ```ruby
237
+ Notey::DeleteOldAttempts.new.call
238
+ ```
239
+
240
+ Everything older than the period goes and everything inside it stays, so
241
+ running it twice leaves the same records as running it once. With no period set
242
+ it refuses to run rather than deleting nothing quietly.
243
+
201
244
  ## Destinations
202
245
 
203
246
  A channel like email reaches a person at an address the app already holds. A
@@ -2,6 +2,10 @@
2
2
 
3
3
  module Notey
4
4
  class Email < Noticed::DeliveryMethod
5
+ def self.reachable?(recipient)
6
+ recipient.email.present?
7
+ end
8
+
5
9
  def deliver
6
10
  NotificationMailer.with(notification: notification, recipient: recipient).notification.deliver_now
7
11
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class DeleteOldAttempts
5
+ UNSET = "notey deletes delivery records older than attempt_retention, which is not set"
6
+
7
+ def call
8
+ raise MissingRetention, UNSET if retention.nil?
9
+
10
+ Attempt.where(created_at: ...retention.ago).delete_all
11
+ end
12
+
13
+ private
14
+
15
+ def retention
16
+ Notey.attempt_retention
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class MarkRead
5
+ def initialize(values:, person: nil, account: nil)
6
+ @person = person
7
+ @account_id = account.respond_to?(:id) ? account.id : account
8
+ @values = values
9
+ end
10
+
11
+ def call
12
+ notification&.mark_as_read!
13
+
14
+ Kept.new
15
+ end
16
+
17
+ private
18
+
19
+ def notification
20
+ Inbox.for(@person, account_id: @account_id).find_by(id: @values[:read])
21
+ end
22
+ end
23
+ end
@@ -27,7 +27,8 @@ module Notey
27
27
  config = ActiveSupport::OrderedOptions.new
28
28
  config[:class] = channel.delivery_method if channel.delivery_method
29
29
  config[:notey_channel] = channel.name
30
- config[:if] = Notey.sends(notey_notification_type, on: channel.name, addressed: channel.addressed?)
30
+ config[:if] = Notey.sends(notey_notification_type, on: channel.name, addressed: channel.addressed?,
31
+ via: Notey.known_delivery_method(channel))
31
32
  config
32
33
  end
33
34
  private_class_method :delivery_config
@@ -0,0 +1,16 @@
1
+ <% notifications = Notey::Inbox.for(person, account_id: account).includes(:event).order(created_at: :desc) %>
2
+
3
+ <% notifications.each do |notification| %>
4
+ <%= ui_panel do %>
5
+ <div data-notification-id="<%= notification.id %>">
6
+ <%= notification.event.title %>
7
+ <%= ui_badge(label: notification.read? ? "Read" : "Unread") %>
8
+ <% unless notification.read? %>
9
+ <%= ui_form(action: submit_url, method: :patch) do %>
10
+ <%= hidden_field_tag "read", notification.id, id: nil %>
11
+ <%= ui_button(label: "Mark read") %>
12
+ <% end %>
13
+ <% end %>
14
+ </div>
15
+ <% end %>
16
+ <% end %>
data/lib/notey/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Notey
2
- VERSION = "0.2.0"
2
+ VERSION = "0.4.0"
3
3
  end
data/lib/notey.rb CHANGED
@@ -19,9 +19,11 @@ module Notey
19
19
  class MissingSender < StandardError; end
20
20
  class UnsendableChannel < StandardError; end
21
21
  class UnsendableAttempt < StandardError; end
22
+ class MissingRetention < StandardError; end
22
23
 
23
24
  class << self
24
25
  attr_writer :notification_url, :mailer_sender
26
+ attr_accessor :attempt_retention
25
27
  end
26
28
 
27
29
  def self.mailer_sender
@@ -95,6 +97,12 @@ module Notey
95
97
  "the channel #{channel.name} needs the option #{required.first}, which notey does not supply"
96
98
  end
97
99
 
100
+ def self.known_delivery_method(channel)
101
+ delivery_method_for(channel)
102
+ rescue UnsendableChannel
103
+ nil
104
+ end
105
+
98
106
  def self.delivery_method_for(channel)
99
107
  name = channel.delivery_method || "Noticed::DeliveryMethods::#{channel.name.camelize}"
100
108
  name.to_s.constantize
@@ -108,11 +116,12 @@ module Notey
108
116
  raise MissingSender, "notey sends digests by email and no mailer_sender is set"
109
117
  end
110
118
 
111
- def self.sends(notification_type, on:, addressed: false)
119
+ def self.sends(notification_type, on:, addressed: false, via: nil)
112
120
  lambda do
113
121
  decision = Channels.decision_for(recipient, notification_type, account_id: event.account_id)
114
122
 
115
123
  next false unless decision.window == "immediate" && decision.channels.include?(on.to_s)
124
+ next false if via.respond_to?(:reachable?) && !via.reachable?(recipient)
116
125
  next true unless addressed
117
126
 
118
127
  Destinations.for(event.account_id, on, member: recipient).present?
@@ -2,7 +2,7 @@
2
2
  name: notey-develop
3
3
  description: Use PROACTIVELY for defining a notification type, sending one to the recipients your own code resolved, sending one when a domain event fires, addressing a channel at a person, scheduling daily and weekly digest runs, reading a person's inbox, and reading their channels and window — MUST BE USED instead of hand-rolling preference checks, per-person delivery addresses, or a digest loop.
4
4
  tools: Read, Write, Edit, Grep
5
- scope: notifications — application-wide channel registrations, notification types that name no channel, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a record of what was sent on each channel
5
+ scope: notifications — application-wide channel registrations, notification types that name no channel, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a record of what was sent on each channel that the host keeps for a period it sets
6
6
  ---
7
7
 
8
8
  This local follows the steps below exactly and invents no others. Where a step
@@ -40,8 +40,16 @@ This local assumes the gem is already hooked into the app; if it is not, that is
40
40
  - `Notey::DigestRun` — sends one window's digests, with `call` for everyone on
41
41
  that window and `deliver_to_member(member, account_id)` for one person in one
42
42
  account.
43
+ - `Notey::DeleteOldAttempts` — deletes every record of what was sent that is
44
+ older than `Notey.attempt_retention`, and refuses to run when no retention is
45
+ set.
43
46
  - `Notey::Inbox.for(member, account_id:)` — returns that person's notifications
44
47
  for that account, as a relation.
48
+ - `notey/inbox` — the partial a host renders inside a page of its own to show a
49
+ person their notifications, taking `person`, `account` and the `submit_url`
50
+ that marking one read posts to.
51
+ - `Notey::MarkRead` — marks the notification a person picked as read, and marks
52
+ nothing when the id belongs to another person or another account.
45
53
  - `wants?(type, on:, account_id:)` — on the recipient model, true when that
46
54
  person wants that type on that channel in that account.
47
55
  - `channels_for(type, account_id:)` — on the recipient model, the channels that
@@ -133,7 +141,21 @@ This local assumes the gem is already hooked into the app; if it is not, that is
133
141
  in this app — cron, a scheduler gem, or a platform scheduler — and at what
134
142
  hour each window should go out.
135
143
 
136
- 7. Read a person's notifications through the inbox rather than querying Noticed
144
+ 7. Schedule the deletion of old delivery records alongside the windows:
145
+
146
+ ```ruby
147
+ Notey::DeleteOldAttempts.new.call
148
+ ```
149
+
150
+ notey writes one record per notification per channel that leaves the
151
+ application and deletes none of them on its own, so a host that never runs
152
+ this keeps every row forever. It deletes everything older than
153
+ `Notey.attempt_retention` and refuses to run when that is not set, which
154
+ `notey-install` is the step that sets. Running it twice leaves the same
155
+ records as running it once, so a schedule that overlaps itself is safe. Ask
156
+ the developer how often it should run.
157
+
158
+ 8. Read a person's notifications through the inbox rather than querying Noticed
137
159
  directly:
138
160
 
139
161
  ```ruby
@@ -143,7 +165,7 @@ This local assumes the gem is already hooked into the app; if it is not, that is
143
165
  It returns a relation, so chain `.unread`, `.order` and `.limit` onto it. With
144
166
  no account it returns an empty relation, never another account's rows.
145
167
 
146
- 8. Read a person's preferences from the host's own code when the app needs to
168
+ 9. Read a person's preferences from the host's own code when the app needs to
147
169
  branch on them:
148
170
 
149
171
  ```ruby
@@ -156,7 +178,7 @@ This local assumes the gem is already hooked into the app; if it is not, that is
156
178
  three read a person's stored row for that type, and fall back to email and
157
179
  in-app when they have never stored one.
158
180
 
159
- 9. In tests that register their own channels or notification types, call
181
+ 10. In tests that register their own channels or notification types, call
160
182
  `Notey.reset!` in teardown. Both are held for the life of the process, so a
161
183
  test that skips this leaves its channels and types in place for every test
162
184
  after it.
@@ -185,7 +207,7 @@ This local assumes the gem is already hooked into the app; if it is not, that is
185
207
  no row saying it went out.
186
208
  - **Notification types, subscribers and digest schedules all belong in the
187
209
  host's code**, never in the gem.
188
- - **Out of scope.** Registering the channels the application has, making a model
189
- a recipient, setting the current person and account, storing an account's
190
- addresses, and the pages a person picks their own channels on all belong to
191
- `notey-install`.
210
+ - **Out of scope.** Registering the channels the application has, setting how
211
+ long delivery records are kept, making a model a recipient, setting the
212
+ current person and account, storing an account's addresses, and the pages a
213
+ person picks their own channels on all belong to `notey-install`.
@@ -2,7 +2,7 @@
2
2
  name: notey-info
3
3
  description: Use to learn what notey offers — notification types and the channels they allow, per-person per-account delivery preferences, digest windows, an in-app inbox, and per-account destinations.
4
4
  tools: Read
5
- scope: notifications — application-wide channel registrations, notification types that name no channel, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a record of what was sent on each channel
5
+ scope: notifications — application-wide channel registrations, notification types that name no channel, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a record of what was sent on each channel that the host keeps for a period it sets
6
6
  ---
7
7
 
8
8
  This local explains what notey is and which of its other two locals you need. It
@@ -80,4 +80,5 @@ has already been sent, you are building, and `notey-develop` owns it.
80
80
  - **Attempt** — one row per notification per outbound channel, claimed before the
81
81
  send and marked after, holding whether it was sent and what a failure said. It
82
82
  is what answers why one person was not reached, and it is what stops the same
83
- channel sending twice for the same notification.
83
+ channel sending twice for the same notification. The host says how long these
84
+ are kept and runs the deletion that enforces it.
@@ -2,7 +2,7 @@
2
2
  name: notey-install
3
3
  description: Use to hook notey into a project — installing its tables, mounting its engine, registering the channels the application can send on, making a model a recipient, setting the current person and account, putting the account on Noticed's rows, supplying a notification url, and reaching the preferences, inbox and destinations pages.
4
4
  tools: Bash, Read, Edit
5
- scope: notifications — application-wide channel registrations, notification types that name no channel, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a record of what was sent on each channel
5
+ scope: notifications — application-wide channel registrations, notification types that name no channel, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a record of what was sent on each channel that the host keeps for a period it sets
6
6
  ---
7
7
 
8
8
  This local follows the steps below exactly and invents no others. Where a step
@@ -33,6 +33,9 @@ different notifications in each.
33
33
  through it.
34
34
  - `Notey.notification_url=` — takes a lambda returning the host's own url for
35
35
  one notification, used to link the rows in a digest email.
36
+ - `Notey.attempt_retention=` — takes how long notey keeps the record of what it
37
+ sent on each channel, such as `90.days`. Nothing is deleted until the host
38
+ runs the deletion.
36
39
  - `Notey.check!` — raises when a registered channel names a delivery method that
37
40
  does not exist or needs an option notey does not supply, and when notey has no
38
41
  sender address for its digests. notey runs it itself after initialization when
@@ -162,7 +165,18 @@ moves all three.
162
165
  the engine's copy. Ask the developer whether these pages should carry the
163
166
  app's navigation.
164
167
 
165
- 12. Set up destinations only if a registered channel needs an address. The stored credential is encrypted, so
168
+ 12. Set the retention for delivery records in
169
+ `config/initializers/notey.rb`:
170
+
171
+ ```ruby
172
+ Notey.attempt_retention = 90.days
173
+ ```
174
+
175
+ Ask the developer how long this application should keep the record of what
176
+ was sent on each channel; there is no default. Scheduling the deletion is
177
+ `notey-develop`'s step, and nothing is deleted until it runs.
178
+
179
+ 13. Set up destinations only if a registered channel needs an address. The stored credential is encrypted, so
166
180
  Active Record encryption keys must be configured in the host's credentials
167
181
  (`bin/rails db:encryption:init` generates a set) or saving a destination
168
182
  raises. Ask the developer whether any channel needs this before doing it.
@@ -190,6 +204,10 @@ moves all three.
190
204
  that changes it.
191
205
  - **Re-run `bin/rails notey:install:migrations` after upgrading the gem.** It
192
206
  copies only the migrations the host does not already have.
193
- - **Out of scope.** Scheduling the digest runs, writing a notification type,
207
+ - **Nothing deletes a delivery record until the host runs the deletion.** The
208
+ table grows for every notification on every outbound channel, so a host that
209
+ sets no retention and schedules no deletion keeps every row forever.
210
+ - **Out of scope.** Scheduling the digest runs and the deletion of old delivery
211
+ records, writing a notification type,
194
212
  sending one when a domain event fires, and reading the inbox from the host's
195
213
  own code all belong to `notey-develop`.
@@ -1,4 +1,4 @@
1
- scope: notifications — application-wide channel registrations, notification types that name no channel, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a record of what was sent on each channel
1
+ scope: notifications — application-wide channel registrations, notification types that name no channel, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a record of what was sent on each channel that the host keeps for a period it sets
2
2
 
3
3
  install:
4
4
  - bin/rails notey:install:migrations
@@ -8,6 +8,7 @@ install:
8
8
  - Notey::Notification
9
9
  - Notey::Current
10
10
  - Notey.notification_url=
11
+ - Notey.attempt_retention=
11
12
  - Notey.check!
12
13
  - /notey/preferences
13
14
  - /notey/notifications
@@ -19,7 +20,10 @@ develop:
19
20
  - notify
20
21
  - Notey::Destinations.for
21
22
  - Notey::DigestRun
23
+ - Notey::DeleteOldAttempts
22
24
  - Notey::Inbox.for
25
+ - notey/inbox
26
+ - Notey::MarkRead
23
27
  - wants?
24
28
  - channels_for
25
29
  - digest_window_for
@@ -40,6 +44,9 @@ sources:
40
44
  - app/models/notey/destination.rb
41
45
  - app/models/notey/digest.rb
42
46
  - app/models/notey/attempt.rb
47
+ - app/models/notey/delete_old_attempts.rb
48
+ - app/models/notey/mark_read.rb
49
+ - app/views/notey/_inbox.html.erb
43
50
  - app/delivery_methods/notey/email.rb
44
51
  - app/delivery_methods/notey/in_app.rb
45
52
  - app/jobs/notey/digest_job.rb
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: notey
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tylercschneider
@@ -88,9 +88,11 @@ files:
88
88
  - app/models/notey/application_record.rb
89
89
  - app/models/notey/attempt.rb
90
90
  - app/models/notey/current.rb
91
+ - app/models/notey/delete_old_attempts.rb
91
92
  - app/models/notey/destination.rb
92
93
  - app/models/notey/digest.rb
93
94
  - app/models/notey/kept.rb
95
+ - app/models/notey/mark_read.rb
94
96
  - app/models/notey/notification.rb
95
97
  - app/models/notey/preference.rb
96
98
  - app/models/notey/refusal.rb
@@ -98,6 +100,7 @@ files:
98
100
  - app/models/notey/save_my_destination.rb
99
101
  - app/models/notey/save_preferences.rb
100
102
  - app/views/notey/_destinations.html.erb
103
+ - app/views/notey/_inbox.html.erb
101
104
  - app/views/notey/_my_destinations.html.erb
102
105
  - app/views/notey/_preferences.html.erb
103
106
  - app/views/notey/destinations/show.html.erb