Skip to content

Commit 6032cb8

Browse files
authored
Merge pull request #160 from joyofrails/feat/subscribers
Add newsletter subscriptions from scratch
2 parents 1e591e7 + 0c39163 commit 6032cb8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+907
-135
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ gem "rouge", group: [:default, :wasm] # Pure Ruby syntaix highlighter [https://g
3535
gem "sitepress-rails", group: [:default, :wasm] # Static site generator for Rails [https://sitepress.cc/getting-started/rails]
3636
gem "phlex-rails", group: [:default, :wasm] # An object-oriented alternative to ActionView for Ruby on Rails. [https://github.com/phlex-ruby/phlex-rails]
3737
gem "commonmarker", require: false
38+
gem "invisible_captcha", group: [:default, :wasm] # Unobtrusive and flexible spam protection for Rails apps [https://github.com/markets/invisible_captcha]
3839

3940
gem "bootsnap", require: false # Reduces boot times through caching; required in config/boot.rb [https://github.com/Shopify/bootsnap]
4041

Gemfile.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ GEM
237237
inline_svg (1.9.0)
238238
activesupport (>= 3.0)
239239
nokogiri (>= 1.6)
240+
invisible_captcha (2.3.0)
241+
rails (>= 5.2)
240242
io-console (0.7.2)
241243
irb (1.13.1)
242244
rdoc (>= 4.0.0)
@@ -562,6 +564,7 @@ DEPENDENCIES
562564
fog-aws
563565
honeybadger
564566
inline_svg
567+
invisible_captcha
565568
js
566569
letter_opener
567570
litestream

app/content/layouts/article.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@
2424
</div>
2525
</article>
2626
<section>
27-
<%= render "newsletter/banner" %>
27+
<%= render "users/newsletter_subscriptions/banner" %>
2828
</section>
2929
<% end %>

app/content/pages/index.html.erb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ title: Joy of Rails
99
<% end %>
1010
</div>
1111
</section>
12-
<section><%= render "newsletter/banner" %></section>
12+
<section>
13+
<%= render "users/newsletter_subscriptions/banner" %>
14+
</section>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
class ApplicationController < ActionController::Base
2+
include Erroring
23
include Authentication
34
end

app/controllers/concerns/authentication.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ def authenticate_user!
2121
redirect_to new_users_session_path, alert: "You need to sign in to access that page" unless user_signed_in?
2222
end
2323

24+
def authenticate_user_or_not_found!
25+
user_signed_in? || not_found!
26+
end
27+
2428
def redirect_admin_if_authenticated
2529
redirect_to admin_root_path, alert: "You are already logged in." if admin_user_signed_in?
2630
end

app/controllers/concerns/erroring.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module Erroring
2+
def not_found!
3+
raise ActionController::RoutingError, "Not Found"
4+
end
5+
end
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
class Users::NewsletterSubscriptionsController < ApplicationController
2+
invisible_captcha only: [:create]
3+
4+
before_action :authenticate_user_or_not_found!, only: [:index, :subscribe]
5+
before_action :authenticate_user!, only: [:subscribe]
6+
before_action :redirect_if_authenticated, only: [:create]
7+
8+
def index
9+
@newsletter_subscription = current_user.newsletter_subscription || current_user.build_newsletter_subscription
10+
11+
render Users::NewsletterSubscriptions::ShowView.new(newsletter_subscription: @newsletter_subscription, show_unsubscribe: true)
12+
end
13+
14+
def show
15+
@newsletter_subscription = NewsletterSubscription.find(params[:id]) or raise ActiveRecord::RecordNotFound
16+
17+
render Users::NewsletterSubscriptions::ShowView.new(newsletter_subscription: @newsletter_subscription, show_unsubscribe: false)
18+
end
19+
20+
def new
21+
user = current_user || User.new
22+
@newsletter_subscription = user.newsletter_subscription || user.build_newsletter_subscription
23+
24+
render Users::NewsletterSubscriptions::NewView.new(newsletter_subscription: @newsletter_subscription)
25+
end
26+
27+
def create
28+
create_user_params = params.require(:user).permit(:email)
29+
@user = User.find_or_initialize_by(email: create_user_params[:email]) do |u|
30+
u.subscribing = true
31+
end
32+
33+
@newsletter_subscription = @user.newsletter_subscription || @user.build_newsletter_subscription
34+
35+
@user.save
36+
37+
if @user.errors.any?
38+
return render Users::NewsletterSubscriptions::NewView.new(newsletter_subscription: @newsletter_subscription), status: :unprocessable_entity
39+
end
40+
41+
if @user.needs_confirmation?
42+
EmailConfirmationNotifier.deliver_to(@user)
43+
else
44+
# TODO: Send already subscribed email
45+
end
46+
47+
redirect_to users_newsletter_subscription_path(@newsletter_subscription)
48+
end
49+
50+
def subscribe
51+
if !current_user.subscribed_to_newsletter?
52+
current_user.create_newsletter_subscription
53+
end
54+
55+
@newsletter_subscription = current_user.newsletter_subscription
56+
57+
if current_user.needs_confirmation?
58+
EmailConfirmationNotifier.deliver_to(current_user)
59+
end
60+
61+
respond_to do |format|
62+
format.html { redirect_to root_path, notice: "Success!" }
63+
format.turbo_stream { redirect_to users_newsletter_subscription_path(@newsletter_subscription) }
64+
end
65+
end
66+
67+
def unsubscribe
68+
if params[:token]
69+
subscription = NewsletterSubscription.find_by_token_for(:unsubscribe, params[:token]) or raise ActiveRecord::RecordNotFound
70+
subscription.destroy
71+
elsif current_user
72+
# Even though we model the subscription as a has_one, we should destroy
73+
# all because has_one is not enforced as a constraint
74+
NewsletterSubscription.where(subscriber: current_user).destroy_all
75+
else
76+
not_found!
77+
end
78+
79+
if request.post? && params["List-Unsubscribe"] == "One-Click"
80+
# must not redirect according to RFC 8058
81+
# could render show action instead
82+
render plain: "You have been unsubscribed", status: :ok
83+
else
84+
respond_to do |format|
85+
format.html { redirect_to root_path, notice: "You have been unsubscribed" }
86+
format.turbo_stream {
87+
redirect_path = current_user ? users_newsletter_subscriptions_path : new_users_newsletter_subscription_path
88+
redirect_to redirect_path
89+
}
90+
end
91+
end
92+
end
93+
end

app/controllers/users/registrations_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# frozen_string_literal: true
22

33
class Users::RegistrationsController < ApplicationController
4+
invisible_captcha only: [:create]
5+
46
before_action :feature_enabled!
57
before_action :redirect_if_authenticated, only: [:create, :new]
68
before_action :authenticate_user!, only: [:edit, :update, :destroy]

app/javascript/css/application.css

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
@import './config/colors-tailwind.css' layer(theme);
66
@import './config/colors-uicolors.css' layer(theme);
77
@import './config/colors.css' layer(theme);
8+
@import './config/dracula.css' layer(theme);
89
@import './config/theme.css' layer(theme);
910
@import './config/settings.css' layer(theme);
1011
@import './baseline.css' layer(theme);
@@ -15,9 +16,9 @@
1516
@import './config/grid.css' layer(layout);
1617
@import './layout.scss' layer(layout);
1718

18-
@import './utilities/tailwind.css';
19-
@import './utilities/tailwind-media.scss';
20-
@import './utilities/custom.css';
19+
@import './utilities/tailwind.css' layer(utilities);
20+
@import './utilities/tailwind-media.scss' layer(utilities);
21+
@import './utilities/custom.css' layer(utilities);
2122

2223
@import './components/logo.css';
2324
@import './components/button.css';
@@ -30,3 +31,4 @@
3031
@import './components/footer-content.css';
3132
@import './components/newsletter-banner.css';
3233
@import './components/anchors.scss';
34+
@import './components/callout.css';

0 commit comments

Comments
 (0)