Skip to content

Commit 0ad109a

Browse files
authored
Manage subscriptions and unsubscribe from all (#85)
1 parent 01f549e commit 0ad109a

File tree

11 files changed

+223
-7
lines changed

11 files changed

+223
-7
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
class Hackathon::Subscriptions::BulkController < ApplicationController
2+
skip_before_action :redirect_if_unauthenticated, only: [:update, :destroy]
3+
4+
before_action :set_user
5+
before_action :set_subscriptions
6+
7+
# Marking subscriptions as active. Used for undoing an "Unsubscribe from all".
8+
# PUT /users/:user_id/subscriptions/bulk
9+
def update
10+
@count = @subscriptions.map(&:resubscribe!).count(true)
11+
12+
redirect_to Hackathon::Subscription.manage_subscriptions_url_for(@user),
13+
notice: "Resubscribed to #{@count} #{"locations".pluralize(@count)}."
14+
end
15+
16+
# Marking subscriptions as inactive.
17+
# DELETE /users/:user_id/subscriptions/bulk
18+
def destroy
19+
@count = @subscriptions.map(&:unsubscribe!).count(true)
20+
21+
redirect_to Hackathon::Subscription.manage_subscriptions_url_for(@user),
22+
notice: "Unsubscribed from #{@count} #{"locations".pluralize(@count)}."
23+
end
24+
25+
private
26+
27+
def set_user
28+
# We're using signed ids here to avoid the need for authentication.
29+
@user = User.find_signed!(params[:user_id], purpose: :manage_subscriptions)
30+
end
31+
32+
def set_subscriptions
33+
@subscriptions = @user.subscriptions.where(id: params[:ids])
34+
end
35+
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
class Hackathon::SubscriptionsController < ApplicationController
2+
skip_before_action :redirect_if_unauthenticated, only: [:index, :unsubscribe_all]
3+
before_action :set_user, only: [:index, :unsubscribe_all]
4+
5+
# Manage subscriptions for a user.
6+
# GET /users/:user_id/subscriptions
7+
def index
8+
return if @expired
9+
10+
@subscriptions = @user.subscriptions.active
11+
end
12+
13+
# Unsubscribe from all subscriptions for a user.
14+
# GET /users/:user_id/subscriptions/unsubscribe_all
15+
def unsubscribe_all
16+
return if @expired
17+
18+
@subscriptions = @user.subscriptions.active
19+
20+
@unsubscribed_ids = @subscriptions.pluck(:id)
21+
@unsubscribe_count = @subscriptions.map(&:unsubscribe!).count(true)
22+
end
23+
24+
private
25+
26+
def set_user
27+
# We're using signed ids here to avoid the need for authentication.
28+
@user = User.find_signed(params[:user_id], purpose: :manage_subscriptions)
29+
@expired = @user.nil?
30+
end
31+
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {Controller} from "@hotwired/stimulus"
2+
3+
export default class extends Controller {
4+
/* Prevent form submission unless required checkboxes are checked.
5+
6+
This controller may require all or some depending on the `require` value.
7+
- `every`: All checkboxes must be checked
8+
- `some`: At least one checkbox must be checked
9+
10+
USAGE:
11+
1. Add the `data-controller` attribute to the form and set the `require`
12+
value to either `every` or `some`.
13+
2. Place the `button` target on the submit button that should be disabled.
14+
3. And place the `input` target and `changed` action on every checkbox
15+
that should be checked.
16+
*/
17+
static targets = ["button", "input"]
18+
static values = {require: String}
19+
20+
connect() {
21+
this.changed()
22+
}
23+
24+
changed() {
25+
const message = {
26+
every: "Please select all options",
27+
some: "Please select at least one option",
28+
}[this.requireValue]
29+
30+
let metRequirements = this.inputTargets[this.requireValue]((input) => input.checked)
31+
32+
this.buttonTarget.disabled = !metRequirements
33+
this.buttonTarget.title = metRequirements ? null : message
34+
}
35+
}

app/mailers/application_mailer.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ class ApplicationMailer < ActionMailer::Base
99

1010
private
1111

12+
def set_unsubscribe_urls_for(user)
13+
@unsubscribe_url = Hackathon::Subscription.unsubscribe_all_url_for user
14+
@email_preferences_url = Hackathon::Subscription.manage_subscriptions_url_for user
15+
end
16+
1217
def set_default_unsubscribe_urls
1318
@unsubscribe_url = root_url
1419
@email_preferences_url = root_url

app/mailers/hackathons/digest_mailer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ def digest
77
.includes(:subscription, hackathon: {logo_attachment: :blob})
88
.group_by(&:subscription)
99

10+
set_unsubscribe_urls_for @recipient
1011
mail to: @recipient.email_address, subject: "Hackathons near you"
1112
end
1213
end

app/models/hackathon/subscription/status.rb

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,36 @@ module Hackathon::Subscription::Status
99
scope :active_for, ->(user) { active.where(subscriber: user) }
1010
end
1111

12-
private
12+
# Unsubscribe URLs must be valid for at least 30 days.
13+
# https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business
14+
UNSUBSCRIBE_EXPIRATION = 2.months
1315

14-
def track_changes
15-
if changed_to_inactive?
16-
record(:disabled)
16+
class_methods do
17+
def manage_subscriptions_url_for(user)
18+
user_signature = user.signed_id purpose: :manage_subscriptions, expires_in: UNSUBSCRIBE_EXPIRATION
19+
Rails.application.routes.url_helpers.user_subscriptions_url(user_signature)
20+
end
21+
22+
def unsubscribe_all_url_for(user)
23+
user_signature = user.signed_id purpose: :manage_subscriptions, expires_in: UNSUBSCRIBE_EXPIRATION
24+
Rails.application.routes.url_helpers.unsubscribe_all_user_subscriptions_url(user_signature)
1725
end
1826
end
1927

20-
def changed_to_inactive?
21-
saved_change_to_status? && inactive?
28+
def unsubscribe
29+
update(status: :inactive)
30+
end
31+
32+
def resubscribe
33+
update(status: :active)
34+
end
35+
36+
private
37+
38+
def track_changes
39+
return unless saved_change_to_status?
40+
41+
record(:enabled) if active?
42+
record(:disabled) if inactive?
2243
end
2344
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<% if subscriptions.present? %>
2+
<%= form_with url: user_subscriptions_bulk_path, method: :delete,
3+
data: {
4+
controller: "forms--required-checkboxes",
5+
"forms--required-checkboxes-require-value": "some"
6+
} do |form| %>
7+
8+
<% subscriptions.each do |subscription| %>
9+
<div class="subscription">
10+
<%= check_box_tag "ids[]", subscription.id, false, data: {
11+
action: "change->forms--required-checkboxes#changed",
12+
"forms--required-checkboxes-target": "input"
13+
}, id: dom_id(subscription) %>
14+
<%= form.label dom_id(subscription), subscription.location %>
15+
</div>
16+
<% end %>
17+
18+
<div>
19+
<%= form.submit "Unsubscribe", data: {"forms--required-checkboxes-target": "button"}, disabled: true %>
20+
or
21+
<%= link_to "unsubscribe from all", unsubscribe_all_user_subscriptions_url %>
22+
</div>
23+
<% end %>
24+
25+
<% else %>
26+
<p>You are not subscribed to any locations.</p>
27+
<% end %>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<% content_for :title, "Manage Subscriptions" %>
2+
3+
<h1>Manage Subscriptions</h1>
4+
5+
<% if @expired %>
6+
<p>This link has expired.</p>
7+
<% else %>
8+
9+
<p>
10+
Hey there, <b><%= @user.first_name || @user.email_address %></b>!
11+
<br/>
12+
You are currently subscribed to the following locations.
13+
</p>
14+
15+
<%= render "manage", subscriptions: @subscriptions %>
16+
<% end %>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<% content_for :title, "Unsubscribe" %>
2+
3+
<h1>Unsubscribe</h1>
4+
5+
<% if @expired %>
6+
<p>This link has expired.</p>
7+
<% else %>
8+
9+
<p>
10+
Hey there, <b><%= @user.name || @user.email_address %></b>!
11+
</p>
12+
13+
<p>
14+
You have unsubscribed from all emails.
15+
<% unless @unsubscribe_count.zero? %>
16+
You were previous subscribed to
17+
<%= @unsubscribe_count %> <%= "location".pluralize(@unsubscribe_count) %>.
18+
<% end %>
19+
20+
<br/>
21+
<i>Sad to see you go. :(</i>
22+
</p>
23+
24+
<%# Button for undo-ing unsubscribe all %>
25+
<% unless @unsubscribe_count.zero? %>
26+
<%= form_with url: user_subscriptions_bulk_path, method: :put do |form| %>
27+
<% @unsubscribed_ids.each do |id| %>
28+
<%= form.hidden_field "ids[]", value: id %>
29+
<% end %>
30+
31+
<%= form.button "Undo" %>
32+
<% end %>
33+
<% end %>
34+
<% end %>

app/views/layouts/application.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<!DOCTYPE html>
22
<html>
33
<head>
4-
<title>Hackathons</title>
4+
<title><%= yield(:title).presence&.+(" — ") %> High School Hackathons</title>
55
<meta name="viewport" content="width=device-width,initial-scale=1">
66
<%= csrf_meta_tags %>
77
<%= csp_meta_tag %>

0 commit comments

Comments
 (0)