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.
- 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
|