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 all 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
31 changes: 27 additions & 4 deletions app/controllers/invite_requests_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,40 @@ 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] = t("invite_requests.resend.not_found")
elsif [email protected]_resend?
flash[:error] = t("invite_requests.resend.not_yet",
count: ArchiveConfig.HOURS_BEFORE_RESEND_INVITATION)
else
@invitation.send_and_set_date(resend: true)

if @invitation.errors.any?
flash[:error] = @invitation.errors.full_messages.first
else
flash[:notice] = t("invite_requests.resend.success", email: @invitation.invitee_email)
end
end

redirect_to status_invite_requests_path
end

# POST /invite_requests
def create
unless AdminSetting.current.invite_from_queue_enabled?
Expand Down
47 changes: 26 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,34 @@ 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)
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_could_not_be_sent, error: e.message)
end

def can_resend?
checked_date = self.resent_at || self.sent_at
checked_date < ArchiveConfig.HOURS_BEFORE_RESEND_INVITATION.hours.ago
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
private

# 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
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>Last resent at</dt>
<dd><%= invitation.resent_at %></dd>
<dt>Redeemed at</dt>
<dd><%= invitation.redeemed_at %></dd>
</dl>
35 changes: 35 additions & 0 deletions app/views/invite_requests/_invitation.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!--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,
contact_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>

<%# Correct heading size for JavaScript display %>
<%= content_for :footer_js do %>
<script>
$j(document).ready(function(){
$j('#invite-heading').replaceWith(function () {
return "<h3>" + $j(this).html() + "</h3>";
});
})
</script>
<% end %>
<!--/content-->
3 changes: 3 additions & 0 deletions app/views/invite_requests/_no_invitation.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<p class="notice">
<%= t(".email_not_found") %>
</p>
10 changes: 8 additions & 2 deletions app/views/invite_requests/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<%= render "invite_request", invite_request: @invite_request %>
<% if @invite_request %>
<%= render "invite_request", invite_request: @invite_request %>
<% elsif @invitation %>
<%= render "invitation", invitation: @invitation %>
<% else %>
<%= render "no_invitation" %>
<% end %>

<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 %>
<%= t(".instructions_html", status_link: link_to("Invitation Request Status page", status_invite_requests_path)) %>
</p>
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("<%= escape_javascript(render "no_invitation") %>");
<% 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
5 changes: 5 additions & 0 deletions config/locales/controllers/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ en:
error: Sorry, that comment could not be unhidden.
permission_denied: Sorry, you don't have permission to unhide that comment.
success: Comment successfully unhidden!
invite_requests:
resend:
not_found: Could not find an invitation associated with that email.
not_yet: You cannot resend an invitation that was sent in the last %{count} hours.
success: Invitation resent to %{email}.
kudos:
create:
success: Thank you for leaving kudos!
Expand Down
5 changes: 5 additions & 0 deletions config/locales/models/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ en:
attributes:
user_defined_tags_count:
at_most: must not add up to more than %{count}. You have entered %{value} of these tags, so you must remove %{diff} of them.
invitation:
attributes:
base:
format: "%{message}"
notification_could_not_be_sent: 'Notification email could not be sent: %{error}'
kudo:
attributes:
commentable:
Expand Down
21 changes: 21 additions & 0 deletions config/locales/views/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -483,10 +483,31 @@ 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 %{contact_support_link}.
other: Because your invitation was resent more than %{count} hours ago, you can have your invitation resent again, or you may want to %{contact_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.
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!
title: Invitation Status for %{email}
no_invitation:
email_not_found: Sorry, we can't find the email address you entered.
show:
instructions_html: To check on the status of your invitation, go to the %{status_link} and enter your email in the space provided.
kudos:
guest_header:
one: "%{count} guest has also left kudos"
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]
51 changes: 38 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,51 @@
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

context "when the email and time are valid" do
let!(:invitation) { create(:invitation) }

it "redirects with a success message" do
travel_to((1 + ArchiveConfig.HOURS_BEFORE_RESEND_INVITATION).hours.from_now)
post :resend, params: { email: invitation.invitee_email }

it_redirects_to_with_notice(status_invite_requests_path,
"Invitation resent to #{invitation.invitee_email}.")
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