acts_as_newsletter 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.md +81 -0
- data/Rakefile +35 -0
- data/lib/acts_as_newsletter.rb +32 -0
- data/lib/acts_as_newsletter/mailer.rb +20 -0
- data/lib/acts_as_newsletter/model.rb +155 -0
- data/lib/acts_as_newsletter/model/config.rb +21 -0
- data/lib/acts_as_newsletter/railtie.rb +19 -0
- data/lib/acts_as_newsletter/version.rb +3 -0
- data/lib/generators/acts_as_newsletter/acts_as_newsletter_generator.rb +35 -0
- data/lib/generators/acts_as_newsletter/install/install_generator.rb +12 -0
- data/lib/generators/acts_as_newsletter/install/templates/initializer.rb +22 -0
- data/lib/generators/acts_as_newsletter/templates/migration.erb +17 -0
- data/lib/tasks/acts_as_newsletter.rake +18 -0
- data/spec/acts_as_newsletter_generator_spec.rb +20 -0
- data/spec/config_spec.rb +23 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +65 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/acts_as_newsletter.rb +6 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/schema.rb +36 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +29 -0
- data/spec/dummy/log/test.log +111434 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/install_generator_spec.rb +18 -0
- data/spec/model_spec.rb +90 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/support/active_record.rb +46 -0
- metadata +219 -0
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
@@ -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,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
|