doubleoptin 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +101 -0
  4. data/Rakefile +32 -0
  5. data/app/assets/config/doubleoptin_manifest.js +2 -0
  6. data/app/assets/javascripts/doubleoptin/application.js +15 -0
  7. data/app/assets/stylesheets/doubleoptin/application.css +16 -0
  8. data/app/assets/stylesheets/doubleoptin/scaffold.css +80 -0
  9. data/app/controllers/doubleoptin/application_controller.rb +5 -0
  10. data/app/controllers/doubleoptin/subscribers_controller.rb +89 -0
  11. data/app/helpers/doubleoptin/application_helper.rb +4 -0
  12. data/app/helpers/doubleoptin/subscribers_helper.rb +4 -0
  13. data/app/jobs/doubleoptin/application_job.rb +4 -0
  14. data/app/mailers/doubleoptin/application_mailer.rb +6 -0
  15. data/app/mailers/doubleoptin/subscription_mailer.rb +19 -0
  16. data/app/models/doubleoptin/application_record.rb +5 -0
  17. data/app/models/doubleoptin/subscriber.rb +60 -0
  18. data/app/views/doubleoptin/subscribers/_form.html.erb +37 -0
  19. data/app/views/doubleoptin/subscribers/confirm.html.slim +2 -0
  20. data/app/views/doubleoptin/subscribers/create.html.slim +4 -0
  21. data/app/views/doubleoptin/subscribers/edit.html.erb +5 -0
  22. data/app/views/doubleoptin/subscribers/index.html.erb +34 -0
  23. data/app/views/doubleoptin/subscribers/new.html.slim +9 -0
  24. data/app/views/doubleoptin/subscribers/unsubscribe.html.slim +2 -0
  25. data/app/views/doubleoptin/subscription_mailer/confirm.html.slim +8 -0
  26. data/app/views/layouts/doubleoptin/application.html.erb +16 -0
  27. data/app/views/layouts/doubleoptin/mailer.html.erb +13 -0
  28. data/config/locales/en.yml +4 -0
  29. data/config/routes.rb +11 -0
  30. data/db/migrate/20190407134153_create_doubleoptin_subscribers.rb +12 -0
  31. data/lib/doubleoptin.rb +8 -0
  32. data/lib/doubleoptin/engine.rb +5 -0
  33. data/lib/doubleoptin/version.rb +3 -0
  34. data/lib/tasks/doubleoptin_tasks.rake +4 -0
  35. metadata +160 -0
@@ -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
@@ -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.
@@ -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).
@@ -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,2 @@
1
+ //= link_directory ../javascripts/doubleoptin .js
2
+ //= link_directory ../stylesheets/doubleoptin .css
@@ -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,5 @@
1
+ module Doubleoptin
2
+ class ApplicationController < ::ApplicationController
3
+ protect_from_forgery with: :exception
4
+ end
5
+ end
@@ -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,4 @@
1
+ module Doubleoptin
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Doubleoptin
2
+ module SubscribersHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Doubleoptin
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Doubleoptin
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ 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,5 @@
1
+ module Doubleoptin
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ 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,2 @@
1
+ h2 Welcome
2
+ p You have successfully confirmed your email address for this list.
@@ -0,0 +1,4 @@
1
+ h2
2
+ ' Thank you for your request,
3
+ = @subscriber.name
4
+ p You will now receive an email at your given adddress. This ensures that it is really you who made the request.
@@ -0,0 +1,5 @@
1
+ <h1>Editing Subscriber</h1>
2
+
3
+ <%= render 'form', subscriber: @subscriber %>
4
+
5
+ <%= link_to 'Back', subscribers_path %>
@@ -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,9 @@
1
+ = form_for @subscriber, url: :subscribe do |f|
2
+ .field
3
+ = f.label :name
4
+ = f.text_field :name, required: true, placeholder: "Name"
5
+ .field
6
+ = f.label :email
7
+ = f.text_field :email, required: true, placeholder: "Email"
8
+ .field
9
+ = f.submit 'Subscribe'
@@ -0,0 +1,2 @@
1
+ h2 Goodbye
2
+ p You have successfully unsubscribed your email address from this list.
@@ -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>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5
+ <style>
6
+ /* Email styles need to be inline */
7
+ </style>
8
+ </head>
9
+
10
+ <body>
11
+ <%= yield %>
12
+ </body>
13
+ </html>
@@ -0,0 +1,4 @@
1
+ en:
2
+ subscriber_subscription_mailer:
3
+ confirm:
4
+ subject: 'Please confirm your subscription'
@@ -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
@@ -0,0 +1,8 @@
1
+ require "doubleoptin/engine"
2
+ require 'rails'
3
+ require 'slim-rails'
4
+ require 'sqlite3' if Rails.env.development?
5
+
6
+ module Doubleoptin
7
+ # Your code goes here...
8
+ end
@@ -0,0 +1,5 @@
1
+ module Doubleoptin
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Doubleoptin
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Doubleoptin
2
+ VERSION = '0.2.1'
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :doubleoptin do
3
+ # # Task goes here
4
+ # end
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: []