acts_as_newsletter 0.1

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 (55) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +81 -0
  3. data/Rakefile +35 -0
  4. data/lib/acts_as_newsletter.rb +32 -0
  5. data/lib/acts_as_newsletter/mailer.rb +20 -0
  6. data/lib/acts_as_newsletter/model.rb +155 -0
  7. data/lib/acts_as_newsletter/model/config.rb +21 -0
  8. data/lib/acts_as_newsletter/railtie.rb +19 -0
  9. data/lib/acts_as_newsletter/version.rb +3 -0
  10. data/lib/generators/acts_as_newsletter/acts_as_newsletter_generator.rb +35 -0
  11. data/lib/generators/acts_as_newsletter/install/install_generator.rb +12 -0
  12. data/lib/generators/acts_as_newsletter/install/templates/initializer.rb +22 -0
  13. data/lib/generators/acts_as_newsletter/templates/migration.erb +17 -0
  14. data/lib/tasks/acts_as_newsletter.rake +18 -0
  15. data/spec/acts_as_newsletter_generator_spec.rb +20 -0
  16. data/spec/config_spec.rb +23 -0
  17. data/spec/dummy/README.rdoc +261 -0
  18. data/spec/dummy/Rakefile +7 -0
  19. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  20. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  21. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  22. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  23. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  24. data/spec/dummy/config.ru +4 -0
  25. data/spec/dummy/config/application.rb +65 -0
  26. data/spec/dummy/config/boot.rb +10 -0
  27. data/spec/dummy/config/database.yml +25 -0
  28. data/spec/dummy/config/environment.rb +5 -0
  29. data/spec/dummy/config/environments/development.rb +37 -0
  30. data/spec/dummy/config/environments/production.rb +67 -0
  31. data/spec/dummy/config/environments/test.rb +37 -0
  32. data/spec/dummy/config/initializers/acts_as_newsletter.rb +6 -0
  33. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  34. data/spec/dummy/config/initializers/inflections.rb +15 -0
  35. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  36. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  37. data/spec/dummy/config/initializers/session_store.rb +8 -0
  38. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  39. data/spec/dummy/config/locales/en.yml +5 -0
  40. data/spec/dummy/config/routes.rb +58 -0
  41. data/spec/dummy/db/development.sqlite3 +0 -0
  42. data/spec/dummy/db/schema.rb +36 -0
  43. data/spec/dummy/db/test.sqlite3 +0 -0
  44. data/spec/dummy/log/development.log +29 -0
  45. data/spec/dummy/log/test.log +111434 -0
  46. data/spec/dummy/public/404.html +26 -0
  47. data/spec/dummy/public/422.html +26 -0
  48. data/spec/dummy/public/500.html +25 -0
  49. data/spec/dummy/public/favicon.ico +0 -0
  50. data/spec/dummy/script/rails +6 -0
  51. data/spec/install_generator_spec.rb +18 -0
  52. data/spec/model_spec.rb +90 -0
  53. data/spec/spec_helper.rb +40 -0
  54. data/spec/support/active_record.rb +46 -0
  55. metadata +219 -0
@@ -0,0 +1,20 @@
1
+ Copyright 2013 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,81 @@
1
+ # Acts as Newsletter
2
+
3
+ This gem allows you to quickly implement a newsletter model without coding the
4
+ whole model management logic.
5
+
6
+ ## Installation
7
+
8
+ Just add the gem to your Gemfile and bundle :
9
+
10
+ ```ruby
11
+ gem "acts_as_newsletter"
12
+ ```
13
+
14
+ To configure it, if needed, you can generate the default initializer with :
15
+
16
+ ```bash
17
+ rails generate acts_as_newsletter:install
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ Use the generator to generate the migration for your model :
23
+
24
+ ```bash
25
+ # If your model is Newsletter
26
+ rails generate acts_as_newsletter newsletter
27
+ ```
28
+
29
+ Now just add the `acts_as_newsletter` macro in your model, passing it a block
30
+ which returns a list of e-mail addresses to which the newsletter will be sent.
31
+
32
+ The block is passed the current Newsletter object so you can configure the way
33
+ emails are retrieved for each different newsletter :
34
+
35
+ ```ruby
36
+ class Newsletter < ActiveRecord::Base
37
+ belongs_to :emails_list
38
+
39
+ acts_as_newsletter do |newsletter|
40
+ emails newsletter.emails_list.emails.pluck(:email)
41
+ # Assuming your mailer views are in app/views/newsletters/
42
+ template_path "newsletters"
43
+ # Set newsletters/newsletter.(html|text).erb
44
+ layout "newsletter"
45
+ # Get your mail template dynamically
46
+ template_name newsletter.type.template
47
+ end
48
+ end
49
+ ```
50
+
51
+ ## Sending it
52
+
53
+ Let's suppose you configured your `Newsletter` model, and created your mailer
54
+ view file, so everything is ready to be sent.
55
+
56
+ All what you need to do is to use the provided default rake task :
57
+
58
+ ```bash
59
+ rake acts_as_newsletter:send_next
60
+ ```
61
+
62
+ But wait ... it doesn't do anything by default since we can't know which model
63
+ is actually a newsletter - well we don't want `acts_as_newsletter` to know it -
64
+ and your sending logic may be custom. So the easiest way to configure it is to
65
+ uncomment the specified block in your
66
+ `config/initializers/acts_as_newsletter.rb` file and define your logic, or let
67
+ the default here :
68
+
69
+ ```ruby
70
+ config.send_next = proc {
71
+ Newsletter.send_next!
72
+ }
73
+ ```
74
+
75
+ Now run `rake acts_as_newsletter:send_next` and it should send your e-mails !
76
+
77
+
78
+ ## Licence
79
+
80
+ It uses MIT Licence so you can do whatever you want with it
81
+
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env rake
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+ begin
9
+ require 'rdoc/task'
10
+ rescue LoadError
11
+ require 'rdoc/rdoc'
12
+ require 'rake/rdoctask'
13
+ RDoc::Task = Rake::RDocTask
14
+ end
15
+
16
+ RDoc::Task.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'ActsAsNewsletter'
19
+ rdoc.options << '--line-numbers'
20
+ rdoc.rdoc_files.include('README.rdoc')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
23
+
24
+
25
+ Bundler::GemHelper.install_tasks
26
+
27
+ require 'rspec/core/rake_task'
28
+
29
+ RSpec::Core::RakeTask.new(:spec)
30
+
31
+ Dir[File.expand_path "../lib/tasks/*", __FILE__].each do |task_file|
32
+ load task_file
33
+ end
34
+
35
+ task :default => :spec
@@ -0,0 +1,32 @@
1
+ module ActsAsNewsletter
2
+ mattr_accessor :send_next
3
+
4
+ class Config
5
+ # Define classes config accessors
6
+ %w(model mailer).each do |method|
7
+ define_method(method) do
8
+ ActsAsNewsletter.const_get(method.camelize)
9
+ end
10
+ end
11
+
12
+ def initialize &block
13
+ block.call(self) if block_given?
14
+ end
15
+
16
+ def method_missing method, *args, &block
17
+ if ActsAsNewsletter.respond_to?(method)
18
+ ActsAsNewsletter.send(method, *args, &block)
19
+ else
20
+ super method, *args, &block
21
+ end
22
+ end
23
+ end
24
+
25
+ def self.config &block
26
+ yield Config.new(&block) if block_given?
27
+ end
28
+ end
29
+
30
+ require 'acts_as_newsletter/model'
31
+ require 'acts_as_newsletter/mailer'
32
+ require 'acts_as_newsletter/railtie' if defined?(Rails)
@@ -0,0 +1,20 @@
1
+ module ActsAsNewsletter
2
+ class Mailer < ActionMailer::Base
3
+ # Allows setting a general <From> header by configuring it in the
4
+ # initializer
5
+ #
6
+ cattr_accessor :from
7
+
8
+ # Sends the actual newsletter to the specified email
9
+ #
10
+ def newsletter newsletter, email, mail_config
11
+ @newsletter = newsletter
12
+ @email = email
13
+ mail mail_config.merge(
14
+ to: email,
15
+ subject: newsletter.subject,
16
+ from: (mail_config[:from] or from)
17
+ )
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,155 @@
1
+ require 'acts_as_newsletter/model/config'
2
+ require 'state_machine'
3
+
4
+ # Module allowing to simply configure a model to be sent as a newsletter
5
+ # It can be mixed in an ActiveRecord model by calling the `acts_as_newsletter`
6
+ # macro directly in your class' body
7
+ #
8
+ # @example
9
+ # class Newsletter < ActiveRecord::Base
10
+ # acts_as_newsletter do |newsletter|
11
+ # emails newsletter.emails
12
+ # template_path "emails"
13
+ # template_name "newsletter"
14
+ # layout false
15
+ # end
16
+ # end
17
+ #
18
+ module ActsAsNewsletter
19
+ module Model
20
+ extend ActiveSupport::Concern
21
+
22
+ module ClassMethods
23
+ attr_reader :config_proc
24
+
25
+ def acts_as_newsletter &config
26
+ # Store config proc to be dynamically run when sending a newsletter
27
+ @config_proc = config
28
+
29
+ # Define state machine
30
+ class_eval do
31
+ state_machine :state, initial: :draft do
32
+
33
+ event :written do
34
+ transition draft: :ready
35
+ end
36
+
37
+ event :ready_canceled do
38
+ transition ready: :draft
39
+ end
40
+
41
+ event :prepare_sending do
42
+ transition ready: :sending
43
+ end
44
+ before_transition on: :prepare_sending, do: :prepare_emails
45
+
46
+ event :sending_complete do
47
+ transition sending: :sent
48
+ end
49
+
50
+ state :draft do
51
+ # If readied is set to true, then we transition to the `ready`
52
+ # state so it can be matched when calling `::next_newsletter`
53
+ before_validation do
54
+ written! if readied
55
+ end
56
+ end
57
+
58
+ state :ready do
59
+ before_validation do
60
+ ready_canceled! if !readied
61
+ end
62
+ end
63
+
64
+ state :sending do
65
+ # When we're sending, saving serializes current emails list
66
+ # to only store remaining recipients and updates sent counter
67
+ #
68
+ before_validation do
69
+ if chunk_sent
70
+ self.recipients = emails.join("|")
71
+ self.sent_count += emails.length
72
+ # Transition to :sent state when complete
73
+ sending_complete! if sent_count == recipients_count
74
+ end
75
+ end
76
+
77
+ # Avoids multiple calls to save to run into the above
78
+ # before_validation hook without the next chunk being really sent
79
+ #
80
+ after_save do
81
+ self.chunk_sent = false if chunk_sent
82
+ @emails = nil
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ # Send next newsletter if one is ready
90
+ #
91
+ def send_next!
92
+ (newsletter = next_newsletter) and newsletter.send_newsletter!
93
+ end
94
+
95
+ # Finds first newsletter being sent or ready
96
+ #
97
+ def next_newsletter
98
+ where(state: :sending).first or where(state: :ready).first
99
+ end
100
+ end
101
+
102
+ # Emails chunk size to send at a time
103
+ #
104
+ mattr_accessor :emails_chunk_size
105
+ self.emails_chunk_size = 500
106
+
107
+ # Boolean allowing us to know if we sent the last emails chunk
108
+ #
109
+ attr_accessor :chunk_sent
110
+
111
+ # Newsletter configuration passed to the block
112
+ def newsletter_config
113
+ @newsletter_config ||=
114
+ Model::Config.new(self, &self.class.config_proc).config
115
+ end
116
+
117
+
118
+ # Prepare model to handle e-mail sending collecting e-mails and
119
+ # initializing counters
120
+ #
121
+ def prepare_emails
122
+ emails_list = newsletter_config[:emails]
123
+ self.recipients_count = emails_list.length
124
+ self.sent_count = 0
125
+ self.recipients = emails_list.join("|")
126
+ end
127
+
128
+ # Parses all available e-mails stored in recpients field
129
+ def available_emails
130
+ @available_emails ||= recipients.split("|")
131
+ end
132
+
133
+ # Takes emails from the list and delete them from it
134
+ def emails
135
+ @emails ||= available_emails.shift(emails_chunk_size)
136
+ end
137
+
138
+ def send_newsletter!
139
+ prepare_sending! if state_name == :ready
140
+ # Get config from newsletter config
141
+ mail_config_keys = [:template_path, :template_name, :layout, :from]
142
+ config = newsletter_config.select do |key, value|
143
+ mail_config_keys.include?(key) and value
144
+ end
145
+
146
+ # Send e-mail to each recipient
147
+ emails.each do |email|
148
+ ActsAsNewsletter::Mailer.newsletter(self, email, config).deliver
149
+ end
150
+
151
+ self.chunk_sent = true
152
+ save
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,21 @@
1
+ module ActsAsNewsletter
2
+ module Model
3
+ class Config
4
+ attr_reader :config, :model
5
+
6
+ def initialize model, &block
7
+ # Initialize config default values
8
+ @config = { emails: [], layout: false }
9
+ @model = model
10
+ # Eval block to edit config
11
+ instance_eval &block if block_given?
12
+ end
13
+
14
+ %w(emails template_path template_name layout from).each do |method|
15
+ define_method method do |value|
16
+ @config[method.to_sym] = value
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ require 'acts_as_newsletter'
2
+
3
+ module ActsAsNewsletter
4
+ require 'rails'
5
+
6
+ class Railtie < Rails::Railtie
7
+ initializer 'acts_as_newsletter.insert_into_active_record' do |app|
8
+ ActiveSupport.on_load :active_record do
9
+ ActiveRecord::Base.send(:include, ActsAsNewsletter::Model)
10
+ end
11
+
12
+ ActiveSupport.on_load :action_controller do
13
+ ActsAsNewsletter::Mailer.send(:add_template_helper, ::ApplicationHelper)
14
+ end
15
+ end
16
+
17
+ rake_tasks { load "tasks/acts_as_newsletter.rake" }
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module ActsAsNewsletter
2
+ VERSION = "0.1"
3
+ end
@@ -0,0 +1,35 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ class ActsAsNewsletterGenerator < ActiveRecord::Generators::Base
4
+ # Copied files come from templates folder
5
+ source_root File.expand_path('../templates', __FILE__)
6
+
7
+ # Generator desc
8
+ desc "ActsAsNewsletter install generator"
9
+
10
+ def generate_migration
11
+ migration_template "migration.erb", "db/migrate/#{ migration_file_name }"
12
+ end
13
+
14
+ def notice
15
+ say " ** Migration created, now just add `acts_as_newsletter` macro to your #{ camelized_model } model"
16
+ end
17
+
18
+ private
19
+
20
+ def camelized_model
21
+ name.camelize
22
+ end
23
+
24
+ def migration_name
25
+ "add_acts_as_newsletter_to_#{ name.pluralize }"
26
+ end
27
+
28
+ def migration_file_name
29
+ "#{ migration_name }.rb"
30
+ end
31
+
32
+ def migration_class_name
33
+ migration_name.camelize
34
+ end
35
+ end
@@ -0,0 +1,12 @@
1
+ module ActsAsNewsletter
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ # Copied files come from templates folder
5
+ source_root File.expand_path('../templates', __FILE__)
6
+
7
+ def copy_initializer
8
+ copy_file "initializer.rb", "config/initializers/acts_as_newsletter.rb"
9
+ end
10
+ end
11
+ end
12
+ end