acts_as_newsletter 0.1

Sign up to get free protection for your applications and to get access to all the features.
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