Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AO3-6609 Allow users to resend invitations #4648

Merged
merged 11 commits into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions app/controllers/invite_requests_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,35 @@ def index
# GET /invite_requests/1
def show
@invite_request = InviteRequest.find_by(email: params[:email])
@position_in_queue = @invite_request.position if @invite_request.present?
unless (request.xml_http_request?) || @invite_request
flash[:error] = "You can search for the email address you signed up with below. If you can't find it, your invitation may have already been emailed to that address; please check your email spam folder as your spam filters may have placed it there."
redirect_to status_invite_requests_path and return

if @invite_request.present?
@position_in_queue = @invite_request.position
else
@invitation = Invitation.unredeemed.from_queue.find_by(invitee_email: params[:email])
end

respond_to do |format|
format.html
format.js
end
end

def resend
@invitation = Invitation.unredeemed.from_queue.find_by(invitee_email: params[:email])

if @invitation.nil?
flash[:error] = ts("Could not find an invitation associated with that email.")
elsif @invitation.can_resend?
@invitation.send_and_set_date(resend: true)
flash[:notice] = ts("Invitation resent to %{email}.", email: @invitation.invitee_email)
else
flash[:error] = ts("You cannot resend an invitation that was sent in the last %{count} hours.",
count: ArchiveConfig.HOURS_BEFORE_RESEND_INVITATION)
weeklies marked this conversation as resolved.
Show resolved Hide resolved
end

redirect_to status_invite_requests_path
end

# POST /invite_requests
def create
unless AdminSetting.current.invite_from_queue_enabled?
Expand Down
49 changes: 28 additions & 21 deletions app/models/invitation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ def recipient_is_not_registered
scope :unsent, -> { where(invitee_email: nil, redeemed_at: nil) }
scope :unredeemed, -> { where('invitee_email IS NOT NULL and redeemed_at IS NULL') }
scope :redeemed, -> { where('redeemed_at IS NOT NULL') }
scope :from_queue, -> { where(external_author: nil).where(creator_type: [nil, "Admin"]) }

before_validation :generate_token, on: :create
after_save :send_and_set_date
after_save :send_and_set_date, if: :saved_change_to_invitee_email?
after_save :adjust_user_invite_status

#Create a certain number of invitations for all valid users
Expand Down Expand Up @@ -54,30 +55,36 @@ def mark_as_redeemed(user=nil)
save
end

private
def send_and_set_date(resend: false)
return if invitee_email.blank?

def generate_token
self.token = Digest::SHA1.hexdigest([Time.now, rand].join)
begin
weeklies marked this conversation as resolved.
Show resolved Hide resolved
if self.external_author
archivist = self.external_author.external_creatorships.collect(&:archivist).collect(&:login).uniq.join(", ")
# send invite synchronously for now -- this should now work delayed but just to be safe
UserMailer.invitation_to_claim(self.id, archivist).deliver_now
else
# send invitations actively sent by a user synchronously to avoid delays
UserMailer.invitation(self.id).deliver_now
end

date_column = resend ? :resent_at : :sent_at
# Skip callbacks within after_save by using update_column to avoid a callback loop
self.update_column(date_column, Time.current)
rescue StandardError => e
errors.add(:base, "Notification email could not be sent: #{e.message}")
weeklies marked this conversation as resolved.
Show resolved Hide resolved
end
end

def send_and_set_date
if self.saved_change_to_invitee_email? && !self.invitee_email.blank?
begin
if self.external_author
archivist = self.external_author.external_creatorships.collect(&:archivist).collect(&:login).uniq.join(", ")
# send invite synchronously for now -- this should now work delayed but just to be safe
UserMailer.invitation_to_claim(self.id, archivist).deliver_now
else
# send invitations actively sent by a user synchronously to avoid delays
UserMailer.invitation(self.id).deliver_now
end
def can_resend?
checked_date = self.resent_at || self.sent_at
checked_date < ArchiveConfig.HOURS_BEFORE_RESEND_INVITATION.hours.ago
end

# Skip callbacks within after_save by using update_column to avoid a callback loop
self.update_column(:sent_at, Time.now)
rescue Exception => exception
errors.add(:base, "Notification email could not be sent: #{exception.message}")
end
end
private

def generate_token
self.token = Digest::SHA1.hexdigest([Time.current, rand].join)
end

#Update the user's out_of_invites status
Expand Down
2 changes: 2 additions & 0 deletions app/views/invitations/_invitation.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
<dd><%= invitation.created_at %></dd>
<dt>Sent at</dt>
<dd><%= invitation.sent_at %></dd>
<dt>Resent at</dt>
weeklies marked this conversation as resolved.
Show resolved Hide resolved
<dd><%= invitation.resent_at %></dd>
<dt>Redeemed at</dt>
<dd><%= invitation.redeemed_at %></dd>
</dl>
34 changes: 34 additions & 0 deletions app/views/invite_requests/_invitation.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!--Descriptive page name, messages and instructions-->
<h2 class="heading" id="invite-heading">
<%= t(".title", email: invitation.invitee_email) %>
</h2>
<!--/descriptions-->

<!--main content-->
<% status = invitation.resent_at ? "resent" : "not_resent" %>
<p>
<%= t(".info.#{status}",
sent_at: l(invitation.sent_at.to_date),
resent_at: invitation.resent_at ? l(invitation.resent_at.to_date) : nil) %>
<% if invitation.can_resend? %>
<%# i18n-tasks-use t("invite_requests.invitation.after_cooldown_period.not_resent")
i18n-tasks-use t("invite_requests.invitation.after_cooldown_period.resent")-%>
<%= t(".after_cooldown_period.#{status}",
count: ArchiveConfig.HOURS_BEFORE_RESEND_INVITATION,
support_link: link_to(t(".contact_support"), new_feedback_report_path)) %>
<%= button_to t(".resend_button"), resend_invite_requests_path(email: invitation.invitee_email) %>
<% else %>
<%= t(".before_cooldown_period", count: ArchiveConfig.HOURS_BEFORE_RESEND_INVITATION) %>
<% end %>
</p>

<%= content_for :footer_js do %>
<script>
$j(document).ready(function(){
$j('#invite-heading').replaceWith(function () {
return "<h3>" + $j(this).html() + "</h3>";
});
})
</script>
<% end %>
<!--/content-->
8 changes: 7 additions & 1 deletion app/views/invite_requests/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
<%= render "invite_request", invite_request: @invite_request %>
<% if @invite_request %>
<%= render "invite_request", invite_request: @invite_request %>
<% elsif @invitation %>
<%= render "invitation", invitation: @invitation %>
<% else %>
<p class="notice">Sorry, we can't find the email address you entered.</p>
<% end %>
weeklies marked this conversation as resolved.
Show resolved Hide resolved

<p>
<%= ts("To check on the status of your invitation, go to the %{status_page} and enter your email in the space provided!", status_page: link_to("Invitation Request Status page", status_invite_requests_path)).html_safe %>
Expand Down
4 changes: 3 additions & 1 deletion app/views/invite_requests/show.js.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<% if @invite_request %>
$j("#invite-status").html("<%= escape_javascript(render "invite_requests/invite_request", invite_request: @invite_request) %>");
<% elsif @invitation %>
$j("#invite-status").html("<%= escape_javascript(render "invitation", invitation: @invitation) %>");
<% else %>
$j("#invite-status").html("<p>Sorry, we can't find the email address you entered. If you had used it to join our invitation queue, it's possible that your invitation may have already been emailed to you; please check your spam folder, as your spam filters may have placed it there.</p>");
$j("#invite-status").html("<p>Sorry, we can't find the email address you entered.</p>");
weeklies marked this conversation as resolved.
Show resolved Hide resolved
<% end %>
2 changes: 2 additions & 0 deletions config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ DELIMITER_FOR_OUTPUT: ', '
INVITE_FROM_QUEUE_ENABLED: true
INVITE_FROM_QUEUE_NUMBER: 10
INVITE_FROM_QUEUE_FREQUENCY: 7

HOURS_BEFORE_RESEND_INVITATION: 24
# this is whether or not people without invitations can create accounts
ACCOUNT_CREATION_ENABLED: false
DAYS_TO_PURGE_UNACTIVATED: 7
Expand Down
17 changes: 17 additions & 0 deletions config/locales/views/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,23 @@ en:
invitation:
email_address_label: Enter an email address
invite_requests:
invitation:
after_cooldown_period:
not_resent:
one: Because your invitation was sent more than an hour ago, you can have your invitation resent.
other: Because your invitation was sent more than %{count} hours ago, you can have your invitation resent.
resent:
one: Because your invitation was resent more than an hour ago, you can have your invitation resent again, or you may want to %{support_link}.
weeklies marked this conversation as resolved.
Show resolved Hide resolved
other: Because your invitation was resent more than %{count} hours ago, you can have your invitation resent again, or you may want to %{support_link}.
before_cooldown_period:
one: if it has been more than an hour since you should have received your invitation, but you have not received it after checking your spam folder, you can visit this page to resend the invitation.
other: if it has been more than %{count} hours since you should have received your invitation, but you have not received it after checking your spam folder, you can visit this page to resend the invitation.
weeklies marked this conversation as resolved.
Show resolved Hide resolved
contact_support: contact Support
info:
not_resent: Your invitation was emailed to this address on %{sent_at}. If you can't find it, please check your email spam folder as your spam filters may have placed it there.
resent: Your invitation was emailed to this address on %{sent_at} and resent on %{resent_at}. If you can't find it, please check your email spam folder as your spam filters may have placed it there.
resend_button: Resend Invitation
title: Invitation Status for %{email}
invite_request:
date: 'At our current rate, you should receive an invitation on or around: %{date}.'
position_html: You are currently number %{position} on our waiting list!
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
collection do
get :manage
get :status
post :resend
end
end

Expand Down
7 changes: 7 additions & 0 deletions db/migrate/20231027172035_add_resent_at_to_invitations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddResentAtToInvitations < ActiveRecord::Migration[6.1]
uses_departure! if Rails.env.staging? || Rails.env.production?

def change
add_column :invitations, :resent_at, :datetime
end
end
30 changes: 28 additions & 2 deletions features/other_a/invite_queue.feature
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Feature: Invite queue management
# check your place in the queue - invalid address
When I check how long "[email protected]" will have to wait in the invite request queue
Then I should see "Invitation Request Status"
And I should see "If you can't find it, your invitation may have already been emailed to that address; please check your email spam folder as your spam filters may have placed it there."
And I should see "Sorry, we can't find the email address you entered."
And I should not see "You are currently number"

# check your place in the queue - correct address
Expand Down Expand Up @@ -98,7 +98,7 @@ Feature: Invite queue management
Then 1 email should be delivered to [email protected]
When I check how long "[email protected]" will have to wait in the invite request queue
Then I should see "Invitation Request Status"
And I should see "If you can't find it, your invitation may have already been emailed to that address;"
And I should see "If you can't find it, please check your email spam folder as your spam filters may have placed it there."

# invite can be used
When I am logged in as an admin
Expand Down Expand Up @@ -155,3 +155,29 @@ Feature: Invite queue management
And I fill in "invite_request_email" with "[email protected]"
And I press "Add me to the list"
Then I should see "Email is already being used by an account holder."

Scenario: Users can resend their invitation after enough time has passed
Given account creation is enabled
And the invitation queue is enabled
And account creation requires an invitation
And the invite_from_queue_at is yesterday
And an invitation request for "[email protected]"
When the scheduled check_invite_queue job is run
Then 1 email should be delivered to [email protected]

When I check how long "[email protected]" will have to wait in the invite request queue
Then I should see "Invitation Request Status"
And I should see "If you can't find it, please check your email spam folder as your spam filters may have placed it there."
And I should not see "Because your invitation was sent more than 24 hours ago, you can have your invitation resent."
And I should not see a "Resend Invitation" button

When all emails have been delivered
And it is currently 25 hours from now
And I check how long "[email protected]" will have to wait in the invite request queue
Then I should see "Invitation Request Status"
And I should see "If you can't find it, please check your email spam folder as your spam filters may have placed it there."
And I should see "Because your invitation was sent more than 24 hours ago, you can have your invitation resent."
And I should see a "Resend Invitation" button

When I press "Resend Invitation"
Then 1 email should be delivered to [email protected]
39 changes: 26 additions & 13 deletions spec/controllers/invite_requests_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,14 @@

describe "GET #show" do
context "when given invalid emails" do
it "redirects to index with error" do
message = "You can search for the email address you signed up with below. If you can't find it, your invitation may have already been emailed to that address; please check your email spam folder as your spam filters may have placed it there."
get :show, params: { id: 0 }
it_redirects_to_with_error(status_invite_requests_path, message)
expect(assigns(:invite_request)).to be_nil
get :show, params: { id: 0, email: "[email protected]" }
it_redirects_to_with_error(status_invite_requests_path, message)
it "renders" do
get :show, params: { email: "[email protected]" }
expect(response).to render_template("show")
expect(assigns(:invite_request)).to be_nil
end

it "renders for an ajax call" do
get :show, params: { id: 0 }, xhr: true
expect(response).to render_template("show")
expect(assigns(:invite_request)).to be_nil
get :show, params: { id: 0, email: "[email protected]" }, xhr: true
get :show, params: { email: "[email protected]" }, xhr: true
expect(response).to render_template("show")
expect(assigns(:invite_request)).to be_nil
end
Expand All @@ -41,19 +34,39 @@
let(:invite_request) { create(:invite_request) }

it "renders" do
get :show, params: { id: 0, email: invite_request.email }
get :show, params: { email: invite_request.email }
expect(response).to render_template("show")
expect(assigns(:invite_request)).to eq(invite_request)
end

it "renders for an ajax call" do
get :show, params: { id: 0, email: invite_request.email }, xhr: true
get :show, params: { email: invite_request.email }, xhr: true
expect(response).to render_template("show")
expect(assigns(:invite_request)).to eq(invite_request)
end
end
end

describe "POST #resend" do
context "when the email doesn't match any invitations" do
it "redirects with an error" do
post :resend, params: { email: "[email protected]" }
it_redirects_to_with_error(status_invite_requests_path,
"Could not find an invitation associated with that email.")
end
end

context "when the invitation is too recent" do
let(:invitation) { create(:invitation) }

it "redirects with an error" do
post :resend, params: { email: invitation.invitee_email }
it_redirects_to_with_error(status_invite_requests_path,
"You cannot resend an invitation that was sent in the last 24 hours.")
end
end
end

describe "POST #create" do
it "redirects to index with error given invalid emails" do
post :create, params: { invite_request: { email: "wat" } }
Expand Down
Loading