doubleoptin 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +101 -0
- data/Rakefile +32 -0
- data/app/assets/config/doubleoptin_manifest.js +2 -0
- data/app/assets/javascripts/doubleoptin/application.js +15 -0
- data/app/assets/stylesheets/doubleoptin/application.css +16 -0
- data/app/assets/stylesheets/doubleoptin/scaffold.css +80 -0
- data/app/controllers/doubleoptin/application_controller.rb +5 -0
- data/app/controllers/doubleoptin/subscribers_controller.rb +89 -0
- data/app/helpers/doubleoptin/application_helper.rb +4 -0
- data/app/helpers/doubleoptin/subscribers_helper.rb +4 -0
- data/app/jobs/doubleoptin/application_job.rb +4 -0
- data/app/mailers/doubleoptin/application_mailer.rb +6 -0
- data/app/mailers/doubleoptin/subscription_mailer.rb +19 -0
- data/app/models/doubleoptin/application_record.rb +5 -0
- data/app/models/doubleoptin/subscriber.rb +60 -0
- data/app/views/doubleoptin/subscribers/_form.html.erb +37 -0
- data/app/views/doubleoptin/subscribers/confirm.html.slim +2 -0
- data/app/views/doubleoptin/subscribers/create.html.slim +4 -0
- data/app/views/doubleoptin/subscribers/edit.html.erb +5 -0
- data/app/views/doubleoptin/subscribers/index.html.erb +34 -0
- data/app/views/doubleoptin/subscribers/new.html.slim +9 -0
- data/app/views/doubleoptin/subscribers/unsubscribe.html.slim +2 -0
- data/app/views/doubleoptin/subscription_mailer/confirm.html.slim +8 -0
- data/app/views/layouts/doubleoptin/application.html.erb +16 -0
- data/app/views/layouts/doubleoptin/mailer.html.erb +13 -0
- data/config/locales/en.yml +4 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20190407134153_create_doubleoptin_subscribers.rb +12 -0
- data/lib/doubleoptin.rb +8 -0
- data/lib/doubleoptin/engine.rb +5 -0
- data/lib/doubleoptin/version.rb +3 -0
- data/lib/tasks/doubleoptin_tasks.rake +4 -0
- metadata +160 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 72d38a228113115e7fb2caec4c671a55f721fded7fa5f54ff916b9c702b5f0d7
|
4
|
+
data.tar.gz: e3f8c4be6684e93a0320aee08d3bdbe0d1ecf33743a4a7f944f242663d062baf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 62f1a3d9fb536d0aeda8cb24ee5ab41c7bd1a55876a4f4370a46ca18ccce1537a201bfc61ce7f3691e5674290b4a4556b962afd106f23ba6fd0589ba1ac42790
|
7
|
+
data.tar.gz: b05a588cd894474059ab1c2de7c81c8afeacd1d66dad408efc48d701d9af29ada462cc0de857a6a96af082c0c4d65412bd1f86281a7913b4412a1370d641b630
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2019 Miguel San Miguel
|
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,101 @@
|
|
1
|
+
# Doubleoptin
|
2
|
+
Doubleoptin is a Rails Engine which allows to collect subscribers to a mailing list through a [double opt-in](https://en.wikipedia.org/wiki/Opt-in_email#Confirmed_opt-in_(COI)/Double_opt-in_(DOI)) process. This is useful for instance if you want to send them newsletters yourself, instead of using some external service.
|
3
|
+
|
4
|
+
Doubleoptin does not manage the authentication of these subscribers along the site (for that use case you probably need something like [devise](https://github.com/plataformatec/devise/) with the `:confirmable` option). It doesn't help you with the Mailer either, which is pretty improved and straightforward in Rails. It just cares about gathering email addresses and sending mails with links for opting in and out of the mailing list.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
If you are running on *Rails 5.2.* (other versions are untested), add this line to your application's Gemfile:
|
8
|
+
```ruby
|
9
|
+
gem 'doubleoptin'
|
10
|
+
```
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
```bash
|
14
|
+
$ bundle
|
15
|
+
```
|
16
|
+
|
17
|
+
Once installed you need to copy the migrations for the Subscriber model with:
|
18
|
+
```bash
|
19
|
+
$ rails doubleoptin:install:migrations
|
20
|
+
```
|
21
|
+
|
22
|
+
Now you can run the migration:
|
23
|
+
```bash
|
24
|
+
$ rails db:migrate
|
25
|
+
```
|
26
|
+
|
27
|
+
Finally, you must mount Doubleoptin into your app at the `config/routes.rb` file with:
|
28
|
+
```ruby
|
29
|
+
mount Doubleoptin::Engine, at: "/doubleoptin"
|
30
|
+
```
|
31
|
+
|
32
|
+
For other installation options see Rails' [Getting started with Engines](https://edgeguides.rubyonrails.org/engines.html#hooking-into-an-application) guide.
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
### Publicly accessible subscription
|
37
|
+
Visitors of your site wanting to subscribe to your mailing list can leave a name and email address through a form. There is one provided under `/doubleoptin/subscribers/new`, but you probably want to improve on that in your own views.
|
38
|
+
|
39
|
+
Provided that you configured your Rails Mailer properly, the subscribers will then get a mail with a confirmation link that the must follow, before been able to receive further communications from your site. They will also get an unsubscription link, with which they can at any time opt out of the mailings. When they follow any of both links, they will respectively see a welcome or a goodbye page from the engine. Those contents (four views and a mail body) can and should be improved by you, replacing them under or `app/views/doubleoptin/subscription_mailer/confirm.html.erb` or `app/views/doubleoptin/subscribers/VIEW.html.erb`, where VIEW can be one of `new`, `create`, `confirm` and `unsubscribe`.
|
40
|
+
|
41
|
+
### Admin area
|
42
|
+
You can access a list of all subscribers under `/doubleoptin/admin/subscribers`. There you can modify or delete subscribers at will, *handle responsibly*!
|
43
|
+
|
44
|
+
This admin area might (and should) be secured if you have an `authenticate` method in your `ApplicationController`, whose policy applies in this area too. You can otherwise implement such a method under `app/controllers/doubleoptin/application_controller.rb` ensuring decoupling between your ApplicationController and Doubleoptin's like so:
|
45
|
+
```ruby
|
46
|
+
module Doubleoptin
|
47
|
+
class ApplicationController < ActionController::Base
|
48
|
+
protect_from_forgery with: :exception
|
49
|
+
def authenticate
|
50
|
+
# your code where
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
### Internal usage
|
57
|
+
The subscribers are programmatically accessible at the model `Doubleoptin::Subscriber`. The `active?` instance method tells whether a subscriber should receive newsletters, and the `active` class scope returns a list of all currently active subscribers (i.e. those who confirmed their address and did not unsubscribe).
|
58
|
+
Duplicate email addresses are allowed as long as the previous ones are all unsubscribed. Addresses are created in lowcase.
|
59
|
+
|
60
|
+
The subscribers can be used to address any newsletters that you may create and send through a mailer doing some job (not part of this engine). For convenience, an `address` method is provided for its usage at the emails' To: field.
|
61
|
+
|
62
|
+
### Hints & Clues
|
63
|
+
- Would you like to mount your main app root onto Doubleoptin's, given that you follow conventions, try this route:
|
64
|
+
```ruby
|
65
|
+
root "doubleoptin/subscribers#new"
|
66
|
+
```
|
67
|
+
- Put your own crafted layout for Doubleoptin views under `app/views/layouts/doubleoptin/application.html.erb`. If you need additional ones, try putting this into `app/controllers/doubleoptin/subscribers_controller.rb`:
|
68
|
+
```ruby
|
69
|
+
class Doubleoptin::SubscribersController < ApplicationController
|
70
|
+
layout 'another', only: [:new, :create, :confirm, :unsubscribe]
|
71
|
+
end
|
72
|
+
```
|
73
|
+
There is also one `app/views/layouts/doubleoptin/mailer.html.erb` which might be of interest for you.
|
74
|
+
- If you prefer another subject for the subscription mail, modify it under `config/locales/en.yml` with this content:
|
75
|
+
```yaml
|
76
|
+
en:
|
77
|
+
subscriber_subscription_mailer:
|
78
|
+
confirm:
|
79
|
+
subject: 'Your subject'
|
80
|
+
```
|
81
|
+
|
82
|
+
## Contributing
|
83
|
+
You can freely contribute to Doubleoptin through merge requests or forks.
|
84
|
+
|
85
|
+
The TODO list is long, since this a primary version of the Engine. A not exhaustive one may includes currently:
|
86
|
+
- Adding i18n and allowing for custom content in the views and subscription email
|
87
|
+
- Allowing for a custom class name for Subscribers
|
88
|
+
- Adding some styling to the welcome and goodbye pages
|
89
|
+
- Adding some styling to the admin area
|
90
|
+
- Improving the functionality of the admin area:
|
91
|
+
- Recording timestamps of the confirmation and unsubscription mails
|
92
|
+
- Displaying timestamps
|
93
|
+
- Grouping subscribers by email
|
94
|
+
- Show some statistics
|
95
|
+
- Paginate the index
|
96
|
+
- Hold different mailing lists in a single instance
|
97
|
+
- Refactoring
|
98
|
+
- ...
|
99
|
+
|
100
|
+
## License
|
101
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Doubleoptin'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
load 'rails/tasks/statistics.rake'
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'test'
|
28
|
+
t.pattern = 'test/**/*_test.rb'
|
29
|
+
t.verbose = false
|
30
|
+
end
|
31
|
+
|
32
|
+
task default: :test
|
@@ -0,0 +1,15 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file. JavaScript code in this file should be added after the last require_* statement.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require rails-ujs
|
14
|
+
//= require activestorage
|
15
|
+
//= require_tree .
|
@@ -0,0 +1,16 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
16
|
+
= require scaffold
|
@@ -0,0 +1,80 @@
|
|
1
|
+
body {
|
2
|
+
background-color: #fff;
|
3
|
+
color: #333;
|
4
|
+
margin: 33px;
|
5
|
+
}
|
6
|
+
|
7
|
+
body, p, ol, ul, td {
|
8
|
+
font-family: verdana, arial, helvetica, sans-serif;
|
9
|
+
font-size: 13px;
|
10
|
+
line-height: 18px;
|
11
|
+
}
|
12
|
+
|
13
|
+
pre {
|
14
|
+
background-color: #eee;
|
15
|
+
padding: 10px;
|
16
|
+
font-size: 11px;
|
17
|
+
}
|
18
|
+
|
19
|
+
a {
|
20
|
+
color: #000;
|
21
|
+
}
|
22
|
+
|
23
|
+
a:visited {
|
24
|
+
color: #666;
|
25
|
+
}
|
26
|
+
|
27
|
+
a:hover {
|
28
|
+
color: #fff;
|
29
|
+
background-color: #000;
|
30
|
+
}
|
31
|
+
|
32
|
+
th {
|
33
|
+
padding-bottom: 5px;
|
34
|
+
}
|
35
|
+
|
36
|
+
td {
|
37
|
+
padding: 0 5px 7px;
|
38
|
+
}
|
39
|
+
|
40
|
+
div.field,
|
41
|
+
div.actions {
|
42
|
+
margin-bottom: 10px;
|
43
|
+
}
|
44
|
+
|
45
|
+
#notice {
|
46
|
+
color: green;
|
47
|
+
}
|
48
|
+
|
49
|
+
.field_with_errors {
|
50
|
+
padding: 2px;
|
51
|
+
background-color: red;
|
52
|
+
display: table;
|
53
|
+
}
|
54
|
+
|
55
|
+
#error_explanation {
|
56
|
+
width: 450px;
|
57
|
+
border: 2px solid red;
|
58
|
+
padding: 7px 7px 0;
|
59
|
+
margin-bottom: 20px;
|
60
|
+
background-color: #f0f0f0;
|
61
|
+
}
|
62
|
+
|
63
|
+
#error_explanation h2 {
|
64
|
+
text-align: left;
|
65
|
+
font-weight: bold;
|
66
|
+
padding: 5px 5px 5px 15px;
|
67
|
+
font-size: 12px;
|
68
|
+
margin: -7px -7px 0;
|
69
|
+
background-color: #c00;
|
70
|
+
color: #fff;
|
71
|
+
}
|
72
|
+
|
73
|
+
#error_explanation ul li {
|
74
|
+
font-size: 12px;
|
75
|
+
list-style: square;
|
76
|
+
}
|
77
|
+
|
78
|
+
label {
|
79
|
+
display: block;
|
80
|
+
}
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require_dependency "doubleoptin/application_controller"
|
2
|
+
|
3
|
+
module Doubleoptin
|
4
|
+
class SubscribersController < ApplicationController
|
5
|
+
before_action :authenticate, only: [:index, :edit, :update, :destroy]
|
6
|
+
before_action :set_subscriber, only: [:edit, :update, :destroy]
|
7
|
+
|
8
|
+
def new
|
9
|
+
@subscriber = Doubleoptin::Subscriber.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def create
|
13
|
+
@subscriber = Doubleoptin::Subscriber.new params.require(:subscriber).permit(:name, :email)
|
14
|
+
|
15
|
+
respond_to do |format|
|
16
|
+
if @subscriber.save
|
17
|
+
SubscriptionMailer.confirm(@subscriber).deliver_now
|
18
|
+
format.html
|
19
|
+
else
|
20
|
+
format.html { redirect_to subscription_path }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# POST /confirm?key=1 (not REST)
|
26
|
+
def confirm
|
27
|
+
@subscriber = Doubleoptin::Subscriber.find_by_nonce :confirm, params[:key]
|
28
|
+
|
29
|
+
respond_to do |format|
|
30
|
+
if !@subscriber.unsubscribed && @subscriber.try(:update_attribute, :confirmed, true)
|
31
|
+
format.html
|
32
|
+
else
|
33
|
+
format.html { redirect_to :root }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# POST /unsubscribe?key=1 (not REST)
|
39
|
+
def unsubscribe
|
40
|
+
@subscriber = Doubleoptin::Subscriber.find_by_nonce :unsubscribe, params[:key]
|
41
|
+
|
42
|
+
respond_to do |format|
|
43
|
+
if @subscriber.try :update_attribute, :unsubscribed, true
|
44
|
+
format.html
|
45
|
+
else
|
46
|
+
format.html { redirect_to :root }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def index
|
52
|
+
@subscribers = Subscriber.all
|
53
|
+
end
|
54
|
+
|
55
|
+
def edit
|
56
|
+
end
|
57
|
+
|
58
|
+
def update
|
59
|
+
respond_to do |format|
|
60
|
+
if @subscriber.update params.require(:subscriber).permit(:name, :email, :unsubscribed, :confirmed)
|
61
|
+
format.html { redirect_to subscribers_url, notice: 'Subscriber was successfully updated.' }
|
62
|
+
format.json { render :index, status: :ok }
|
63
|
+
else
|
64
|
+
format.html { render :edit }
|
65
|
+
format.json { render json: @subscriber.errors, status: :unprocessable_entity }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def destroy
|
71
|
+
@subscriber.destroy
|
72
|
+
respond_to do |format|
|
73
|
+
format.html { redirect_to subscribers_url, notice: 'Subscriber was successfully destroyed.' }
|
74
|
+
format.json { head :no_content }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def authenticate
|
79
|
+
super
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def set_subscriber
|
85
|
+
@subscriber = Subscriber.find(params[:id])
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Doubleoptin
|
2
|
+
class SubscriptionMailer < ApplicationMailer
|
3
|
+
layout 'mailer'
|
4
|
+
|
5
|
+
# Subject can be set in your I18n file at config/locales/en.yml
|
6
|
+
# with the following lookup:
|
7
|
+
#
|
8
|
+
# en.subscriber_subscription_mailer.confirm.subject
|
9
|
+
#
|
10
|
+
def confirm(subscriber)
|
11
|
+
@subscriber = subscriber
|
12
|
+
@confirm_link = confirm_url key: @subscriber.confirm_link
|
13
|
+
@unsubscribe_link = unsubscribe_url key: @subscriber.unsubscribe_link
|
14
|
+
|
15
|
+
mail to: @subscriber.address,
|
16
|
+
subject: t('subscriber_subscription_mailer.confirm.subject')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Doubleoptin
|
2
|
+
class Subscriber < ApplicationRecord
|
3
|
+
before_validation :normalize_email, on: :create
|
4
|
+
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
|
5
|
+
validates :email, presence: true, on: :create
|
6
|
+
validates :email, uniqueness: true, on: :create, if: :all_previous_gone?
|
7
|
+
validates :name, presence: true, on: :create
|
8
|
+
|
9
|
+
def normalize_email
|
10
|
+
email.downcase!
|
11
|
+
end
|
12
|
+
|
13
|
+
def all_previous_gone?
|
14
|
+
! self.class.where(email: email).not_gone.empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
def active?
|
18
|
+
confirmed and not unsubscribed or false
|
19
|
+
end
|
20
|
+
|
21
|
+
scope :not_gone, -> { where(unsubscribed: [nil, false]) }
|
22
|
+
scope :active, -> { not_gone.where(confirmed: true) }
|
23
|
+
|
24
|
+
def address
|
25
|
+
"#{name} <#{email}>"
|
26
|
+
end
|
27
|
+
|
28
|
+
def confirm_link
|
29
|
+
@confirm ||= digest :confirm
|
30
|
+
end
|
31
|
+
|
32
|
+
def unsubscribe_link
|
33
|
+
@unsubscribe ||= digest :unsubscribe
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def digest(nonce)
|
39
|
+
self.class.digest(nonce, email)
|
40
|
+
end
|
41
|
+
|
42
|
+
class << self
|
43
|
+
# Return the last subscriber with the email
|
44
|
+
def find_by_nonce(nonce, digest)
|
45
|
+
found = all.map(&:email).find do |email|
|
46
|
+
digest(nonce, email) == digest
|
47
|
+
end
|
48
|
+
self.where(email: found).last
|
49
|
+
end
|
50
|
+
|
51
|
+
# Return a hash digest made of three parts:
|
52
|
+
# - a given nonce
|
53
|
+
# - a given email adddress
|
54
|
+
# - the number of times that the email address appears in the DB
|
55
|
+
def digest(nonce, email)
|
56
|
+
Digest::MD5.hexdigest("#{nonce}:#{where(email: email).size}:#{email}")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
<%= form_with(model: subscriber, local: true) do |form| %>
|
2
|
+
<% if subscriber.errors.any? %>
|
3
|
+
<div id="error_explanation">
|
4
|
+
<h2><%= pluralize(subscriber.errors.count, "error") %> prohibited this subscriber from being saved:</h2>
|
5
|
+
|
6
|
+
<ul>
|
7
|
+
<% subscriber.errors.full_messages.each do |message| %>
|
8
|
+
<li><%= message %></li>
|
9
|
+
<% end %>
|
10
|
+
</ul>
|
11
|
+
</div>
|
12
|
+
<% end %>
|
13
|
+
|
14
|
+
<div class="field">
|
15
|
+
<%= form.label :name %>
|
16
|
+
<%= form.text_field :name %>
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<div class="field">
|
20
|
+
<%= form.label :email %>
|
21
|
+
<%= form.text_field :email %>
|
22
|
+
</div>
|
23
|
+
|
24
|
+
<div class="field">
|
25
|
+
<%= form.label :confirmed %>
|
26
|
+
<%= form.check_box :confirmed %>
|
27
|
+
</div>
|
28
|
+
|
29
|
+
<div class="field">
|
30
|
+
<%= form.label :unsubscribed %>
|
31
|
+
<%= form.check_box :unsubscribed %>
|
32
|
+
</div>
|
33
|
+
|
34
|
+
<div class="actions">
|
35
|
+
<%= form.submit %>
|
36
|
+
</div>
|
37
|
+
<% end %>
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<p id="notice"><%= notice %></p>
|
2
|
+
|
3
|
+
<h1>Subscribers</h1>
|
4
|
+
|
5
|
+
<table>
|
6
|
+
<thead>
|
7
|
+
<tr>
|
8
|
+
<th>Name</th>
|
9
|
+
<th>Email</th>
|
10
|
+
<th>Confirmed</th>
|
11
|
+
<th>Unsubscribed</th>
|
12
|
+
<th>Active</th>
|
13
|
+
<th colspan="2"></th>
|
14
|
+
</tr>
|
15
|
+
</thead>
|
16
|
+
|
17
|
+
<tbody>
|
18
|
+
<% @subscribers.each do |subscriber| %>
|
19
|
+
<tr>
|
20
|
+
<td><%= subscriber.name %></td>
|
21
|
+
<td><%= subscriber.email %></td>
|
22
|
+
<td><%= subscriber.confirmed %></td>
|
23
|
+
<td><%= subscriber.unsubscribed %></td>
|
24
|
+
<td><%= subscriber.active? %></td>
|
25
|
+
<td><%= link_to 'Edit', edit_subscriber_path(subscriber) %></td>
|
26
|
+
<td><%= link_to 'Destroy', subscriber, method: :delete, data: { confirm: 'Are you sure?' } %></td>
|
27
|
+
</tr>
|
28
|
+
<% end %>
|
29
|
+
</tbody>
|
30
|
+
</table>
|
31
|
+
|
32
|
+
<br>
|
33
|
+
|
34
|
+
<%= link_to 'New Subscriber', subscription_path %>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
p Thank you for your interest in our mailing list.
|
2
|
+
p
|
3
|
+
' Please follow this link in order to complete your subscription to it:
|
4
|
+
= link_to @confirm_link, @confirm_link
|
5
|
+
p If you did not subscribe to this list, you can safely just ignore this message.
|
6
|
+
p
|
7
|
+
' You can revoke your subscription at any time following this link:
|
8
|
+
= link_to @unsubscribe_link, @unsubscribe_link
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Doubleoptin</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= stylesheet_link_tag "doubleoptin/application", media: "all" %>
|
9
|
+
<%= javascript_include_tag "doubleoptin/application" %>
|
10
|
+
</head>
|
11
|
+
<body>
|
12
|
+
|
13
|
+
<%= yield %>
|
14
|
+
|
15
|
+
</body>
|
16
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
Doubleoptin::Engine.routes.draw do
|
2
|
+
get :subscription, to: 'subscribers#new'
|
3
|
+
post :subscribe, to: 'subscribers#create'
|
4
|
+
get :confirm, to: 'subscribers#confirm'
|
5
|
+
get :unsubscribe, to: 'subscribers#unsubscribe'
|
6
|
+
|
7
|
+
scope :admin do
|
8
|
+
resources :subscribers, except: [:new, :create]
|
9
|
+
end
|
10
|
+
root to: 'subscribers#new'
|
11
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateDoubleoptinSubscribers < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
create_table :doubleoptin_subscribers do |t|
|
4
|
+
t.string :name
|
5
|
+
t.string :email
|
6
|
+
t.boolean :confirmed, default: nil
|
7
|
+
t.boolean :unsubscribed, default: nil
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/doubleoptin.rb
ADDED
metadata
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: doubleoptin
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Miguel San Miguel
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-04-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.2.3
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.2.3
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: slim-rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: capybara-email
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest-rails-capybara
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: webdrivers
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Rails Engine for collecting subscribers to a mailing list
|
98
|
+
email:
|
99
|
+
- development@miguelsanmiguel.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- MIT-LICENSE
|
105
|
+
- README.md
|
106
|
+
- Rakefile
|
107
|
+
- app/assets/config/doubleoptin_manifest.js
|
108
|
+
- app/assets/javascripts/doubleoptin/application.js
|
109
|
+
- app/assets/stylesheets/doubleoptin/application.css
|
110
|
+
- app/assets/stylesheets/doubleoptin/scaffold.css
|
111
|
+
- app/controllers/doubleoptin/application_controller.rb
|
112
|
+
- app/controllers/doubleoptin/subscribers_controller.rb
|
113
|
+
- app/helpers/doubleoptin/application_helper.rb
|
114
|
+
- app/helpers/doubleoptin/subscribers_helper.rb
|
115
|
+
- app/jobs/doubleoptin/application_job.rb
|
116
|
+
- app/mailers/doubleoptin/application_mailer.rb
|
117
|
+
- app/mailers/doubleoptin/subscription_mailer.rb
|
118
|
+
- app/models/doubleoptin/application_record.rb
|
119
|
+
- app/models/doubleoptin/subscriber.rb
|
120
|
+
- app/views/doubleoptin/subscribers/_form.html.erb
|
121
|
+
- app/views/doubleoptin/subscribers/confirm.html.slim
|
122
|
+
- app/views/doubleoptin/subscribers/create.html.slim
|
123
|
+
- app/views/doubleoptin/subscribers/edit.html.erb
|
124
|
+
- app/views/doubleoptin/subscribers/index.html.erb
|
125
|
+
- app/views/doubleoptin/subscribers/new.html.slim
|
126
|
+
- app/views/doubleoptin/subscribers/unsubscribe.html.slim
|
127
|
+
- app/views/doubleoptin/subscription_mailer/confirm.html.slim
|
128
|
+
- app/views/layouts/doubleoptin/application.html.erb
|
129
|
+
- app/views/layouts/doubleoptin/mailer.html.erb
|
130
|
+
- config/locales/en.yml
|
131
|
+
- config/routes.rb
|
132
|
+
- db/migrate/20190407134153_create_doubleoptin_subscribers.rb
|
133
|
+
- lib/doubleoptin.rb
|
134
|
+
- lib/doubleoptin/engine.rb
|
135
|
+
- lib/doubleoptin/version.rb
|
136
|
+
- lib/tasks/doubleoptin_tasks.rake
|
137
|
+
homepage:
|
138
|
+
licenses:
|
139
|
+
- MIT
|
140
|
+
metadata: {}
|
141
|
+
post_install_message:
|
142
|
+
rdoc_options: []
|
143
|
+
require_paths:
|
144
|
+
- lib
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
requirements: []
|
156
|
+
rubygems_version: 3.0.3
|
157
|
+
signing_key:
|
158
|
+
specification_version: 4
|
159
|
+
summary: Double opt-in for mailings in Ruby on Rails
|
160
|
+
test_files: []
|