Skip to content

Commit 2ecdbd0

Browse files
Streamline organization onboarding using a newly introduced account request flow (#1788)
* Add AccountRequest model + specs & migration * Enable creating account_requests * Redirect to confirmation page when the account_request was succesful * Add case to handle invalid token * Add AccountRequestMailer & confirmation mail * Add routing specs for AccountRequestsController * Just about ready functionally * Fix spec with the mailing * Functional! * Allow mailer jobs to be processed off the `default` queue * Update copies * Change route name * Update styling on pages * Keep track of confirmed account requests * Functional! * Flash an error message if trying to use the same account request * Add more account_request request specs * Add admin request specs * Add system spec * System test for Account Request flow * Fix rubocop complaints * Add back in fakeredis * Remove unneeded templates * Update copy * Fix broken specs * Fix broken specs * Update app/views/static/index.html.erb Co-authored-by: Aaron H <[email protected]> * Update app/views/account_requests/confirmation.html.erb Co-authored-by: Aaron H <[email protected]> * Update app/views/account_requests/confirmation.html.erb Co-authored-by: Aaron H <[email protected]> * Update app/views/account_request_mailer/confirmation.html.erb Co-authored-by: Aaron H <[email protected]> * Update spec * Update based on PR suggestion * Fix rubocop * Fix broken spec Co-authored-by: Aaron H <[email protected]>
1 parent 8f1fc0a commit 2ecdbd0

33 files changed

+989
-12
lines changed

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ gem "image_processing"
3131
gem "jbuilder"
3232
gem "jquery-rails"
3333
gem "jquery-ui-rails"
34+
gem "jwt"
3435
gem "kaminari"
3536
gem "mini_racer", "~> 0.3.1"
3637
gem "momentjs-rails"
@@ -96,6 +97,7 @@ group :test do
9697
gem "rails-controller-testing"
9798
gem "rspec-sidekiq"
9899
gem 'simplecov'
100+
gem 'shoulda-matchers', '~> 4.0'
99101
gem 'webdrivers', '~> 4.4'
100102
gem "webmock", "~> 3.9"
101103
end

Gemfile.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ GEM
220220
thor (>= 0.14, < 2.0)
221221
jquery-ui-rails (6.0.1)
222222
railties (>= 3.2.16)
223+
jwt (2.2.1)
223224
kaminari (1.2.1)
224225
activesupport (>= 4.1.0)
225226
kaminari-actionview (= 1.2.1)
@@ -424,6 +425,8 @@ GEM
424425
rubyzip (>= 1.2.2)
425426
semantic_range (2.3.0)
426427
shellany (0.0.1)
428+
shoulda-matchers (4.3.0)
429+
activesupport (>= 4.2.0)
427430
sidekiq (5.2.9)
428431
connection_pool (~> 2.2, >= 2.2.2)
429432
rack (~> 2.0)
@@ -553,6 +556,7 @@ DEPENDENCIES
553556
jbuilder
554557
jquery-rails
555558
jquery-ui-rails
559+
jwt
556560
kaminari
557561
launchy
558562
letter_opener
@@ -578,6 +582,7 @@ DEPENDENCIES
578582
rubocop
579583
rubocop-rails (~> 2.8.1)
580584
sass-rails
585+
shoulda-matchers (~> 4.0)
581586
sidekiq
582587
sidekiq-scheduler
583588
simple_form
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
class AccountRequestsController < ApplicationController
2+
skip_before_action :authorize_user
3+
skip_before_action :authenticate_user!
4+
5+
before_action :set_account_request_from_token, only: [:received, :confirmation, :confirm]
6+
7+
layout 'devise'
8+
9+
def received; end
10+
11+
def confirmation; end
12+
13+
def confirm
14+
@account_request.update!(confirmed_at: Time.current)
15+
AccountRequestMailer.approval_request(account_request_id: @account_request.id).deliver_later
16+
end
17+
18+
def invalid_token; end
19+
20+
def new
21+
@account_request = AccountRequest.new
22+
end
23+
24+
def create
25+
@account_request = AccountRequest.new(account_request_params)
26+
27+
if @account_request.save
28+
AccountRequestMailer.confirmation(account_request_id: @account_request.id).deliver_later
29+
30+
redirect_to received_account_requests_path(token: @account_request.identity_token),
31+
notice: 'Account request was successfully created.'
32+
else
33+
render :new
34+
end
35+
end
36+
37+
private
38+
39+
# Use callbacks to share common setup or constraints between actions.
40+
def set_account_request_from_token
41+
@account_request = AccountRequest.get_by_identity_token(params[:token])
42+
43+
# Use confirmation timestamp instead
44+
if @account_request.nil? || @account_request.confirmed? || @account_request.processed?
45+
redirect_to invalid_token_account_requests_path(token: params[:token])
46+
return
47+
end
48+
49+
@account_request
50+
end
51+
52+
# Only allow a list of trusted parameters through.
53+
def account_request_params
54+
params.require(:account_request).permit(:name, :email, :organization_name, :organization_website, :request_details)
55+
end
56+
end

app/controllers/admin/organizations_controller.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,22 @@ def index
3030

3131
def new
3232
@organization = Organization.new
33-
@organization.users.build(organization_admin: true)
33+
account_request = params[:token] && AccountRequest.get_by_identity_token(params[:token])
34+
35+
if account_request.blank?
36+
@organization.users.build(organization_admin: true)
37+
elsif account_request.processed?
38+
flash[:error] = "The account request had already been processed and cannot be used again"
39+
@organization.users.build(organization_admin: true)
40+
else
41+
@organization.assign_attributes_from_account_request(account_request)
42+
end
3443
end
3544

3645
def create
37-
@organization = Organization.create(organization_params)
38-
@organization.users.last.update(password: SecureRandom.uuid)
46+
@organization = Organization.new(organization_params)
47+
@organization.users.last.assign_attributes(password: SecureRandom.uuid)
48+
3949
if @organization.save
4050
Organization.seed_items(@organization)
4151
@organization.users.last.invite!
@@ -63,7 +73,7 @@ def destroy
6373

6474
def organization_params
6575
params.require(:organization)
66-
.permit(:name, :short_name, :street, :city, :state, :zipcode, :email, :url, :logo, :intake_location, :default_email_text,
76+
.permit(:name, :short_name, :street, :city, :state, :zipcode, :email, :url, :logo, :intake_location, :default_email_text, :account_request_id,
6777
users_attributes: %i(name email organization_admin))
6878
end
6979
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
class AccountRequestMailer < ApplicationMailer
2+
def confirmation(account_request_id:)
3+
@account_request = AccountRequest.find(account_request_id)
4+
5+
mail(
6+
to: @account_request.email,
7+
subject: '[Action Required] Diaperbase Account Request'
8+
)
9+
end
10+
11+
def approval_request(account_request_id:)
12+
@account_request = AccountRequest.find(account_request_id)
13+
14+
mail(
15+
16+
subject: "[Account Request] #{@account_request.organization_name}"
17+
)
18+
end
19+
end

app/models/account_request.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# == Schema Information
2+
#
3+
# Table name: account_requests
4+
#
5+
# id :bigint not null, primary key
6+
# confirmed_at :datetime
7+
# email :string not null
8+
# name :string not null
9+
# organization_name :string not null
10+
# organization_website :string
11+
# request_details :text not null
12+
# created_at :datetime not null
13+
# updated_at :datetime not null
14+
#
15+
class AccountRequest < ApplicationRecord
16+
validates :name, presence: true
17+
validates :email, presence: true, uniqueness: true
18+
validates :request_details, presence: true, length: { minimum: 50 }
19+
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
20+
21+
validate :email_not_already_used_by_organization
22+
validate :email_not_already_used_by_user
23+
24+
has_one :organization, dependent: :nullify
25+
26+
def self.get_by_identity_token(identity_token)
27+
decrypted_token = JWT.decode(identity_token, Rails.application.secrets[:secret_key_base], true, { algorithm: 'HS256' })
28+
account_request_id = decrypted_token[0]["account_request_id"]
29+
30+
AccountRequest.find_by(id: account_request_id)
31+
rescue StandardError
32+
# The identity_token was determined to not be valid
33+
# and returns nil to indicate no match found.
34+
nil
35+
end
36+
37+
def identity_token
38+
raise 'must have an id' unless persisted?
39+
40+
JWT.encode({ account_request_id: id }, Rails.application.secrets[:secret_key_base], 'HS256')
41+
end
42+
43+
def confirmed?
44+
confirmed_at.present?
45+
end
46+
47+
def processed?
48+
organization.present?
49+
end
50+
51+
private
52+
53+
def email_not_already_used_by_organization
54+
if Organization.find_by(email: email)
55+
errors.add(:email, 'already used by an existing Organization')
56+
end
57+
end
58+
59+
def email_not_already_used_by_user
60+
if User.find_by(email: email)
61+
errors.add(:email, 'already used by an existing User')
62+
end
63+
end
64+
end

app/models/organization.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# zipcode :string
2222
# created_at :datetime not null
2323
# updated_at :datetime not null
24+
# account_request_id :integer
2425
#
2526

2627
class Organization < ApplicationRecord
@@ -113,6 +114,23 @@ def bottom(limit = 5)
113114
scope :alphabetized, -> { order(:name) }
114115
scope :search_name, ->(query) { where('name ilike ?', "%#{query}%") }
115116

117+
def assign_attributes_from_account_request(account_request)
118+
assign_attributes(
119+
name: account_request.organization_name,
120+
url: account_request.organization_website,
121+
email: account_request.email,
122+
account_request_id: account_request.id
123+
)
124+
125+
users.build(
126+
organization_admin: true,
127+
email: account_request.email,
128+
name: account_request.name
129+
)
130+
131+
self
132+
end
133+
116134
# NOTE: when finding Organizations, use Organization.find_by(short_name: params[:organization_id])
117135
def to_param
118136
short_name
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
5+
</head>
6+
<body>
7+
<h1> We've just received a confirmed account request from <%= @account_request.organization_name %> </h1>
8+
9+
<h3> Here are their details </h3>
10+
<table>
11+
<thead>
12+
<td>Attribute Name</td>
13+
<td>Value</td>
14+
</thead>
15+
16+
<% @account_request.attributes.each do |ar, val| %>
17+
<tr>
18+
<td><%= ar.humanize %></td>
19+
<td><%= val %></td>
20+
</tr>
21+
<% end %>
22+
</table>
23+
24+
<br>
25+
<%= link_to 'Create This Organization', new_admin_organization_url(token: @account_request.identity_token) %>
26+
</body>
27+
</html>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
5+
</head>
6+
<body>
7+
<p>Greetings from the Diaperbase Team,</p>
8+
9+
<h3> Click 'Confirm' when you want to continue requesting an account with us </h3>
10+
11+
<%= link_to 'Confirm This Request', confirmation_account_requests_url(token: @account_request.identity_token) %>
12+
13+
<p>
14+
We're delighted to hear from you and hope you're all staying well!
15+
</p>
16+
17+
<p>
18+
First, and most importantly, DiaperBase is 100% free! We're supported by the non-profit Code for Good and Microsoft sponsors the servers!
19+
</p>
20+
21+
<p>
22+
If you'd like to experience the app, please log in to the sandbox/demo sites and test it out.
23+
Here is the login information for the demo sites:
24+
</p>
25+
26+
<p>
27+
<a href='https://diaperbase.org/'>DiaperBase</a>
28+
<br>
29+
<span>Username: [email protected]</span>
30+
<br>
31+
<span>Password: password</span>
32+
</p>
33+
34+
<p>
35+
<a href='https://partnerbase.org/'>PartnerBase</a>
36+
<br>
37+
<span>Username: [email protected]</span>
38+
<br>
39+
<span>Password: password</span>
40+
</p>
41+
42+
<p>
43+
A couple things to know about the sandbox servers before you start using them:
44+
The development team uses the servers for testing our new features and upgrades before putting them on the live site to ensure that no bugs get pushed through to the live site, so if something looks different than the (real) diaper.app site that is probably why! Please don’t enter any sensitive information into the demo servers, several users have access to the demo servers and it will be visible to all users.
45+
</p>
46+
47+
<p>
48+
Finally, we made a getting started video with detailed directions on setting up your diaper bank in DiaperBase, check it out
49+
<a href='https://www.youtube.com/watch?v=fwo3WKMGM_4&feature=youtu.be'>here</a>! Please let us know when you would like to begin using DiaperBase and we will set you up and send you a welcome email.
50+
</p>
51+
52+
<p>Diaperbase Team</p>
53+
54+
</body>
55+
</html>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<div class="card">
2+
<div class="card-body text-center">
3+
<div class='card-text'>
4+
<h3> Confirmed! <h3>
5+
<h4> We will be processing your request now. </h4>
6+
7+
<p> We will send your invitation via email when we've processed your request. We will reach out to you also via email if we have any questions.</p>
8+
</div>
9+
</div>
10+
</div>
11+

0 commit comments

Comments
 (0)