noticed 1.2.8 → 1.2.13

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8612d56239114aba89f0a57365165e8e4eed4ca1a6291392099aede62ff5a852
4
- data.tar.gz: 102c543b03ce37815165f056ce90c6c8ccb42e4cbe771a2e10b7d9c2d69d9245
3
+ metadata.gz: 177af67cbc18228368b1046461e97ec0046ab7be37dcd528d0834456184d3dbc
4
+ data.tar.gz: 4b68f5ad47ac4cde2fb48f0738792c8d70332a0bf3b2a5bc4c6c0a830bd9b030
5
5
  SHA512:
6
- metadata.gz: '094d06187629f0ea3faa41ccd570da239fcad6f94a5de59235d4afa87c623d62aa4635aadde71b7e3f8e6956e57b10e17efd0387005b779ca613e45f57918693'
7
- data.tar.gz: 9b84a30978138d9bf9dbbb440f921fde3bd6b503055eb100d556247e4ca4e7dfdfa775912323f9df6a5103962b994d821ed17367c4e2357dd254567b7a6a1eda
6
+ metadata.gz: 127fb40a8e97d833c01d8f35586aee3ae5be6b673e20670306a5c94982ea4b2f358d7d471020460689483c326c7caf5a9f9bd3854b2c4052f47f2714a4aa8b92
7
+ data.tar.gz: 5f285005c9e5553e9cde3203a7b7b646c3448edb9fdb0335e0286dc2bee14d09f7001644733caa8a5795f5291380fc4e65aa37ab5c89e9c752a54d2dd35a8a61
data/README.md CHANGED
@@ -78,7 +78,7 @@ To add delivery methods, simply `include` the module for the delivery methods yo
78
78
  class CommentNotification < Noticed::Base
79
79
  deliver_by :database
80
80
  deliver_by :action_cable
81
- deliver_by :email, if: :email_notifications?
81
+ deliver_by :email, mailer: 'CommentMailer', if: :email_notifications?
82
82
 
83
83
  # I18n helpers
84
84
  def message
@@ -120,7 +120,7 @@ Like ActiveRecord, notifications have several different types of callbacks.
120
120
  ```ruby
121
121
  class CommentNotification < Noticed::Base
122
122
  deliver_by :database
123
- deliver_by :email
123
+ deliver_by :email, mailer: 'CommentMailer'
124
124
 
125
125
  # Callbacks for the entire delivery
126
126
  before_deliver :whatever
@@ -158,7 +158,7 @@ For example:
158
158
 
159
159
  ```ruby
160
160
  class CommentNotification < Noticed::Base
161
- deliver_by :email, if: :email_notifications?
161
+ deliver_by :email, mailer: 'CommentMailer', if: :email_notifications?
162
162
 
163
163
  def email_notifications?
164
164
  recipient.email_notifications?
@@ -256,7 +256,7 @@ Sends an SMS notification via Twilio.
256
256
 
257
257
  * `credentials: :get_twilio_credentials` - *Optional*
258
258
 
259
- Use a custom method to retrieve the credentials for Twilio. Method should return a Hash with `:account_sid`, `:auth_token` and `:phone_ number` keys.
259
+ Use a custom method to retrieve the credentials for Twilio. Method should return a Hash with `:account_sid`, `:auth_token` and `:phone_number` keys.
260
260
 
261
261
  Defaults to `Rails.application.credentials.twilio[:account_sid]` and `Rails.application.credentials.twilio[:auth_token]`
262
262
 
@@ -311,22 +311,28 @@ Sends an SMS notification via Vonage / Nexmo.
311
311
 
312
312
  ### 🚚 Custom Delivery Methods
313
313
 
314
- You can define a custom delivery method easily by adding a `deliver_by` line with a unique name and class option. The class will be instantiated and should inherit from `Noticed::DeliveryMethods::Base`.
314
+ To generate a custom delivery method, simply run
315
315
 
316
- ```ruby
317
- class MyNotification < Noticed::Base
318
- deliver_by :discord, class: "DiscordNotification"
319
- end
320
- ```
316
+ `rails generate noticed:delivery_method Discord`
317
+
318
+ This will generate a new `DeliveryMethods::Discord` class inside the `app/notifications/delivery_methods` folder, which can be used to deliver notifications to Discord.
321
319
 
322
320
  ```ruby
323
- class DiscordNotification < Noticed::DeliveryMethods::Base
321
+ class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
324
322
  def deliver
325
323
  # Logic for sending a Discord notification
326
324
  end
327
325
  end
328
326
  ```
329
327
 
328
+ You can use the custom delivery method thus created by adding a `deliver_by` line with a unique name and `class` option in your notification class.
329
+
330
+ ```ruby
331
+ class MyNotification < Noticed::Base
332
+ deliver_by :discord, class: "DeliveryMethods::Discord"
333
+ end
334
+ ```
335
+
330
336
  Delivery methods have access to the following methods and attributes:
331
337
 
332
338
  * `notification` - The instance of the Notification. You can call methods on the notification to let the user easily override formatting and other functionality of the delivery method.
@@ -334,12 +340,46 @@ Delivery methods have access to the following methods and attributes:
334
340
  * `recipient` - The object who should receive the notification. This is typically a User, Account, or other ActiveRecord model.
335
341
  * `params` - The params passed into the notification. This is details about the event that happened. For example, a user commenting on a post would have params of `{ user: User.first }`
336
342
 
343
+ #### Validating options passed to Custom Delivery methods
344
+
345
+ You can validate the options passed to the custom delivery method and raise validation errors. This is helpful for debugging to make sure valid and required options were passed in.
346
+
347
+ To do this, simply override the `self.validate!(options)` method from the `Noticed::DeliveryMethods::Base` class in your Custom Delivery method.
348
+
349
+ ```ruby
350
+ class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
351
+ def deliver
352
+ # Logic for sending a Discord notification
353
+ end
354
+
355
+ def self.validate!(options)
356
+ unless options.key?(:sent_by)
357
+ raise Noticed::ValidationError, 'the `sent_by` option is missing'
358
+ end
359
+ end
360
+ end
361
+
362
+ class CommentNotification < Noticed::Base
363
+ deliver_by :discord, class: 'DeliveryMethods::Discord'
364
+ end
365
+ ```
366
+
367
+ Now it will raise an error because a required argument is missing.
368
+
369
+ To fix the error, the argument has to be passed correctly. For example:
370
+
371
+ ```ruby
372
+ class CommentNotification < Noticed::Base
373
+ deliver_by :discord, class: 'DeliveryMethods::Discord', sent_by: User.admin.first
374
+ end
375
+ ```
376
+
337
377
  #### Callbacks
338
378
 
339
379
  Callbacks for delivery methods wrap the *actual* delivery of the notification. You can use `before_deliver`, `around_deliver` and `after_deliver` in your custom delivery methods.
340
380
 
341
381
  ```ruby
342
- class DiscordNotification < Noticed::DeliveryMethods::Base
382
+ class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
343
383
  after_deliver do
344
384
  # Do whatever you want
345
385
  end
@@ -351,13 +391,13 @@ end
351
391
  Rails 6.1+ can serialize Class and Module objects as arguments to ActiveJob. The following syntax should work for Rails 6.1+:
352
392
 
353
393
  ```ruby
354
- deliver_by DiscordNotification
394
+ deliver_by DeliveryMethods::Discord
355
395
  ```
356
396
 
357
397
  For Rails 6.0 and earlier, you must pass strings of the class names in the `deliver_by` options.
358
398
 
359
399
  ```ruby
360
- deliver_by :discord, class: "DiscordNotification"
400
+ deliver_by :discord, class: "DeliveryMethods::Discord"
361
401
  ```
362
402
 
363
403
  We recommend the Rails 6.0 compatible options to prevent confusion.
@@ -401,6 +441,60 @@ Check if read / unread:
401
441
  @notification.unread?
402
442
  ```
403
443
 
444
+ #### Associating Notifications
445
+
446
+ Adding notification associations to your models makes querying and deleting notifications easy and is a pretty critical feature of most applications.
447
+
448
+ For example, in most cases, you'll want to delete notifications for records that are destroyed.
449
+
450
+ ##### JSON Columns
451
+
452
+ If you're using MySQL or Postgresql, the `params` column on the notifications table is in `json` or `jsonb` format and can be queried against directly.
453
+
454
+ For example, we can query the notifications and delete them on destroy like so:
455
+
456
+ ```ruby
457
+ class Post < ApplicationRecord
458
+ def notifications
459
+ # Exact match
460
+ @notifications ||= Notification.where(params: { post: self })
461
+
462
+ # Or Postgres syntax to query the post key in the JSON column
463
+ # @notifications ||= Notification.where("params->'post' = ?", Noticed::Coder.dump(self).to_json)
464
+ end
465
+
466
+ before_destroy :destroy_notifications
467
+
468
+ def destroy_notifications
469
+ notifications.destroy_all
470
+ end
471
+ end
472
+ ```
473
+
474
+ ##### Polymorphic Assocation
475
+
476
+ If your notification is only associated with one model or you're using a `text` column for your params column , then a polymorphic association is what you'll want to use.
477
+
478
+ 1. Add a polymorphic association to the Notification model. `rails g migration AddNotifiableToNotifications notifiable:belongs_to{polymorphic}`
479
+
480
+ 2. Add `has_many :notifications, as: :notifiable, dependent: :destroy` to each model
481
+
482
+ 3. Customize database `format: ` option to write the `notifiable` attribute(s) when saving the notification
483
+
484
+ ```ruby
485
+ class ExampleNotification < Noticed::Base
486
+ deliver_by :database, format: :format_for_database
487
+
488
+ def format_for_database
489
+ {
490
+ notifiable: params.delete(:post),
491
+ type: self.class.name,
492
+ params: params
493
+ }
494
+ end
495
+ end
496
+ ```
497
+
404
498
  ## 🙏 Contributing
405
499
 
406
500
  This project uses [Standard](https://github.com/testdouble/standard) for formatting Ruby code. Please make sure to run `standardrb` before submitting pull requests.
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module Noticed
6
+ module Generators
7
+ class DeliveryMethodGenerator < Rails::Generators::NamedBase
8
+ include Rails::Generators::ResourceHelpers
9
+
10
+ source_root File.expand_path("../templates", __FILE__)
11
+
12
+ desc "Generates a class for a custom delivery method with the given NAME."
13
+
14
+ def generate_notification
15
+ template "delivery_method.rb", "app/notifications/delivery_methods/#{singular_name}.rb"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -15,13 +15,22 @@ module Noticed
15
15
  argument :attributes, type: :array, default: [], banner: "field:type field:type"
16
16
 
17
17
  def generate_notification
18
- generate :model, name, "recipient:references{polymorphic}", "type", "params:text", "read_at:datetime", *attributes
18
+ generate :model, name, "recipient:references{polymorphic}", "type", params_column, "read_at:datetime", *attributes
19
19
  end
20
20
 
21
21
  def add_noticed_model
22
22
  inject_into_class model_path, class_name, " include Noticed::Model\n"
23
23
  end
24
24
 
25
+ def add_not_nullable
26
+ migration_path = Dir.glob(Rails.root.join("db/migrate/*")).max_by { |f| File.mtime(f) }
27
+
28
+ # Force is required because null: false already exists in the file and Thor isn't smart enough to tell the difference
29
+ insert_into_file migration_path, after: "t.string :type", force: true do
30
+ ", null: false"
31
+ end
32
+ end
33
+
25
34
  def done
26
35
  readme "README" if behavior == :invoke
27
36
  end
@@ -31,6 +40,17 @@ module Noticed
31
40
  def model_path
32
41
  @model_path ||= File.join("app", "models", "#{file_path}.rb")
33
42
  end
43
+
44
+ def params_column
45
+ case ActiveRecord::Base.configurations.configs_for(spec_name: "primary").config["adapter"]
46
+ when "mysql"
47
+ "params:json"
48
+ when "postgresql"
49
+ "params:jsonb"
50
+ else
51
+ "params:text"
52
+ end
53
+ end
34
54
  end
35
55
  end
36
56
  end
@@ -12,7 +12,7 @@ module Noticed
12
12
  desc "Generates a notification with the given NAME."
13
13
 
14
14
  def generate_notification
15
- template "notification.rb", "app/notifications/#{singular_name}.rb"
15
+ template "notification.rb", "app/notifications/#{file_path}.rb"
16
16
  end
17
17
  end
18
18
  end
@@ -0,0 +1,12 @@
1
+ class DeliveryMethods::<%= class_name %> < Noticed::DeliveryMethods::Base
2
+ def deliver
3
+ # Logic for sending the notification
4
+ end
5
+
6
+ # You may override this method to validate options for the delivery method
7
+ # Invalid options should raise a ValidationError
8
+ #
9
+ # def self.validate!(options)
10
+ # raise ValidationError, "required_option missing" unless options[:required_option]
11
+ # end
12
+ end
@@ -6,6 +6,7 @@ module Noticed
6
6
  autoload :Base, "noticed/base"
7
7
  autoload :Coder, "noticed/coder"
8
8
  autoload :Model, "noticed/model"
9
+ autoload :TextCoder, "noticed/text_coder"
9
10
  autoload :Translation, "noticed/translation"
10
11
 
11
12
  module DeliveryMethods
@@ -33,4 +34,12 @@ module Noticed
33
34
 
34
35
  class ValidationError < StandardError
35
36
  end
37
+
38
+ class ResponseUnsuccessful < StandardError
39
+ attr_reader :response
40
+
41
+ def initialize(response)
42
+ @response = response
43
+ end
44
+ end
36
45
  end
@@ -69,6 +69,9 @@ module Noticed
69
69
  def run_delivery(recipient, enqueue: true)
70
70
  delivery_methods = self.class.delivery_methods.dup
71
71
 
72
+ # Set recipient to instance var so it is available to Notification class
73
+ @recipient = recipient
74
+
72
75
  # Run database delivery inline first if it exists so other methods have access to the record
73
76
  if (index = delivery_methods.find_index { |m| m[:name] == :database })
74
77
  delivery_method = delivery_methods.delete_at(index)
@@ -94,13 +97,12 @@ module Noticed
94
97
  }
95
98
 
96
99
  run_callbacks delivery_method[:name] do
97
- klass = get_class(delivery_method[:name], delivery_method[:options])
98
- enqueue ? klass.perform_later(args) : klass.perform_now(args)
100
+ method = delivery_method_for(delivery_method[:name], delivery_method[:options])
101
+ enqueue ? method.perform_later(args) : method.perform_now(args)
99
102
  end
100
103
  end
101
104
 
102
- # Retrieves the correct class for a delivery method
103
- def get_class(name, options)
105
+ def delivery_method_for(name, options)
104
106
  if options[:class]
105
107
  options[:class].constantize
106
108
  else
@@ -108,13 +110,25 @@ module Noticed
108
110
  end
109
111
  end
110
112
 
111
- # Validates that all params are present
112
113
  def validate!
114
+ validate_params_present!
115
+ validate_options_of_delivery_methods!
116
+ end
117
+
118
+ # Validates that all params are present
119
+ def validate_params_present!
113
120
  self.class.param_names.each do |param_name|
114
121
  if params[param_name].nil?
115
122
  raise ValidationError, "#{param_name} is missing."
116
123
  end
117
124
  end
118
125
  end
126
+
127
+ def validate_options_of_delivery_methods!
128
+ delivery_methods.each do |delivery_method|
129
+ method = delivery_method_for(delivery_method[:name], delivery_method[:options])
130
+ method.validate!(delivery_method[:options])
131
+ end
132
+ end
119
133
  end
120
134
  end
@@ -2,18 +2,12 @@ module Noticed
2
2
  class Coder
3
3
  def self.load(data)
4
4
  return if data.nil?
5
-
6
- # Text columns need JSON parsing
7
- if data.is_a?(String)
8
- data = JSON.parse(data)
9
- end
10
-
11
5
  ActiveJob::Arguments.send(:deserialize_argument, data)
12
6
  end
13
7
 
14
8
  def self.dump(data)
15
9
  return if data.nil?
16
- ActiveJob::Arguments.send(:serialize_argument, data).to_json
10
+ ActiveJob::Arguments.send(:serialize_argument, data)
17
11
  end
18
12
  end
19
13
  end
@@ -6,15 +6,15 @@ module Noticed
6
6
 
7
7
  attr_reader :notification, :options, :recipient, :record
8
8
 
9
- def perform(notification_class:, options:, params:, recipient:, record:)
10
- @notification = notification_class.constantize.new(params)
11
- @options = options
12
- @recipient = recipient
13
- @record = record
9
+ def perform(args)
10
+ @notification = args[:notification_class].constantize.new(args[:params])
11
+ @options = args[:options]
12
+ @recipient = args[:recipient]
13
+ @record = args[:record]
14
14
 
15
15
  # Make notification aware of database record and recipient during delivery
16
- @notification.record = record
17
- @notification.recipient = recipient
16
+ @notification.record = args[:record]
17
+ @notification.recipient = args[:recipient]
18
18
 
19
19
  run_callbacks :deliver do
20
20
  deliver
@@ -24,6 +24,41 @@ module Noticed
24
24
  def deliver
25
25
  raise NotImplementedError, "Delivery methods must implement a deliver method"
26
26
  end
27
+
28
+ def self.validate!(options)
29
+ # Override this method in your custom DeliveryMethod class to validate the options
30
+ # and raise error, if invalid.
31
+ end
32
+
33
+ private
34
+
35
+ # Helper method for making POST requests from delivery methods
36
+ #
37
+ # Usage:
38
+ # post("http://example.com", basic_auth: {user:, pass:}, json: {}, form: {})
39
+ #
40
+ def post(url, args = {})
41
+ basic_auth = args.delete(:basic_auth)
42
+
43
+ request = if basic_auth
44
+ HTTP.basic_auth(user: basic_auth[:user], pass: basic_auth[:pass])
45
+ else
46
+ HTTP
47
+ end
48
+
49
+ response = request.post(url, args)
50
+
51
+ if options[:debug]
52
+ Rails.logger.debug("POST #{url}")
53
+ Rails.logger.debug("Response: #{response.code}: #{response}")
54
+ end
55
+
56
+ if !options[:ignore_failure] && !response.status.success?
57
+ raise ResponseUnsuccessful.new(response)
58
+ end
59
+
60
+ response
61
+ end
27
62
  end
28
63
  end
29
64
  end
@@ -5,6 +5,12 @@ module Noticed
5
5
  mailer.with(format).send(method.to_sym).deliver_later
6
6
  end
7
7
 
8
+ def self.validate!(options)
9
+ unless options.key?(:mailer)
10
+ raise ValidationError, "email delivery method requires a 'mailer' to be specified"
11
+ end
12
+ end
13
+
8
14
  private
9
15
 
10
16
  def mailer
@@ -2,7 +2,7 @@ module Noticed
2
2
  module DeliveryMethods
3
3
  class Slack < Base
4
4
  def deliver
5
- HTTP.post(url, json: format)
5
+ post(url, json: format)
6
6
  end
7
7
 
8
8
  private
@@ -2,7 +2,7 @@ module Noticed
2
2
  module DeliveryMethods
3
3
  class Twilio < Base
4
4
  def deliver
5
- HTTP.basic_auth(user: account_sid, pass: auth_token).post(url, form: format)
5
+ post(url, basic_auth: {user: account_sid, pass: auth_token}, form: format)
6
6
  end
7
7
 
8
8
  private
@@ -2,7 +2,11 @@ module Noticed
2
2
  module DeliveryMethods
3
3
  class Vonage < Base
4
4
  def deliver
5
- HTTP.post("https://rest.nexmo.com/sms/json", json: format)
5
+ response = post("https://rest.nexmo.com/sms/json", json: format)
6
+ status = response.parse.dig("messages", 0, "status")
7
+ if !options[:ignore_failure] && status != "0"
8
+ raise ResponseUnsuccessful.new(response)
9
+ end
6
10
  end
7
11
 
8
12
  private
@@ -5,17 +5,28 @@ module Noticed
5
5
  included do
6
6
  self.inheritance_column = nil
7
7
 
8
- serialize :params, Noticed::Coder
8
+ serialize :params, noticed_coder
9
9
 
10
10
  belongs_to :recipient, polymorphic: true
11
11
 
12
12
  scope :newest_first, -> { order(created_at: :desc) }
13
+ scope :unread, -> { where(read_at: nil) }
14
+ scope :read, -> { where.not(read_at: nil) }
13
15
  end
14
16
 
15
17
  module ClassMethods
16
18
  def mark_as_read!
17
19
  update_all(read_at: Time.current, updated_at: Time.current)
18
20
  end
21
+
22
+ def noticed_coder
23
+ case attribute_types["params"].type
24
+ when :json, :jsonb
25
+ Noticed::Coder
26
+ else
27
+ Noticed::TextCoder
28
+ end
29
+ end
19
30
  end
20
31
 
21
32
  # Rehydrate the database notification into the Notification object for rendering
@@ -0,0 +1,16 @@
1
+ module Noticed
2
+ class TextCoder
3
+ def self.load(data)
4
+ return if data.nil?
5
+
6
+ # Text columns need JSON parsing
7
+ data = JSON.parse(data)
8
+ ActiveJob::Arguments.send(:deserialize_argument, data)
9
+ end
10
+
11
+ def self.dump(data)
12
+ return if data.nil?
13
+ ActiveJob::Arguments.send(:serialize_argument, data).to_json
14
+ end
15
+ end
16
+ end
@@ -1,21 +1,23 @@
1
- module Translation
2
- extend ActiveSupport::Concern
1
+ module Noticed
2
+ module Translation
3
+ extend ActiveSupport::Concern
3
4
 
4
- # Returns the +i18n_scope+ for the class. Overwrite if you want custom lookup.
5
- def i18n_scope
6
- :notifications
7
- end
5
+ # Returns the +i18n_scope+ for the class. Overwrite if you want custom lookup.
6
+ def i18n_scope
7
+ :notifications
8
+ end
8
9
 
9
- def translate(key, **options)
10
- I18n.translate(scope_translation_key(key), **options)
11
- end
12
- alias t translate
10
+ def translate(key, **options)
11
+ I18n.translate(scope_translation_key(key), **options)
12
+ end
13
+ alias t translate
13
14
 
14
- def scope_translation_key(key)
15
- if key.to_s.start_with?(".")
16
- "#{i18n_scope}.#{self.class.name.underscore}#{key}"
17
- else
18
- key
15
+ def scope_translation_key(key)
16
+ if key.to_s.start_with?(".")
17
+ "#{i18n_scope}.#{self.class.name.underscore}#{key}"
18
+ else
19
+ key
20
+ end
19
21
  end
20
22
  end
21
23
  end
@@ -1,3 +1,3 @@
1
1
  module Noticed
2
- VERSION = "1.2.8"
2
+ VERSION = "1.2.13"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: noticed
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.8
4
+ version: 1.2.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Oliver
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-07 00:00:00.000000000 Z
11
+ date: 2020-08-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -92,9 +92,11 @@ files:
92
92
  - README.md
93
93
  - Rakefile
94
94
  - app/channels/noticed/notification_channel.rb
95
+ - lib/generators/noticed/delivery_method_generator.rb
95
96
  - lib/generators/noticed/model_generator.rb
96
97
  - lib/generators/noticed/notification_generator.rb
97
98
  - lib/generators/noticed/templates/README
99
+ - lib/generators/noticed/templates/delivery_method.rb.tt
98
100
  - lib/generators/noticed/templates/notification.rb.tt
99
101
  - lib/noticed.rb
100
102
  - lib/noticed/base.rb
@@ -109,6 +111,7 @@ files:
109
111
  - lib/noticed/delivery_methods/vonage.rb
110
112
  - lib/noticed/engine.rb
111
113
  - lib/noticed/model.rb
114
+ - lib/noticed/text_coder.rb
112
115
  - lib/noticed/translation.rb
113
116
  - lib/noticed/version.rb
114
117
  - lib/tasks/noticed_tasks.rake