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-6712 Allow invites to be resent and index invitee_email #4791

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
54 changes: 33 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,41 @@ 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

# Skip callbacks within after_save by using update_column to avoid a callback loop
if resend
attrs = { resent_at: Time.current }
# This applies to old invites when AO3-6094 wasn't fixed.
attrs[:sent_at] = self.created_at if self.sent_at.nil?
self.update_columns(attrs)
else
self.update_column(:sent_at, Time.current)
end
rescue StandardError => e
errors.add(:base, :notification_could_not_be_sent, error: e.message)
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?
# created_at fallback is a vestige of the already fixed AO3-6094.
checked_date = self.resent_at || self.sent_at || self.created_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>Last resent at</dt>
<dd><%= invitation.resent_at %></dd>
<dt>Redeemed at</dt>
<dd><%= invitation.redeemed_at %></dd>
</dl>
28 changes: 28 additions & 0 deletions app/views/invite_requests/_invitation.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!--Descriptive page name, messages and instructions-->
<h2 class="heading" id="invite-heading">
<%= t(".title", email: invitation.invitee_email) %>
</h2>
<!--/descriptions-->

<!--main content-->
<p>
<% status = invitation.resent_at ? "resent" : "not_resent" %>
<%= t(".info.#{status}",
sent_at: l((invitation.sent_at || invitation.created_at).to_date),
resent_at: invitation.resent_at ? l(invitation.resent_at.to_date) : nil) %>
</p>

<p>
<% 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_html")-%>
<% status = invitation.resent_at ? "resent_html" : "not_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>
<!--/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>
11 changes: 10 additions & 1 deletion app/views/invite_requests/show.js.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
<% 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) %>");

<%# Correct heading size for JavaScript display %>
$j(document).ready(function(){
$j('#invite-heading').replaceWith(function () {
return '<h3 class="heading">' + $j(this).html() + "</h3>";
});
})
<% 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 %>
10 changes: 6 additions & 4 deletions app/views/invite_requests/status.html.erb
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<!--Descriptive page name, messages and instructions-->
<h2 class="heading">
<%= ts("Invitation Request Status") %>
<%= t(".heading") %>
</h2>

<p>
<%= ts("There are currently %{count} people on the waiting list.", count: InviteRequest.count) %>
<%= t(".waiting_list", count: InviteRequest.count) %>
<% if AdminSetting.current.invite_from_queue_enabled? %>
<%= ts("We are sending out %{invites} invitations per day.", invites: AdminSetting.current.invite_from_queue_number) %>
<%= t(".send_rate", invites: AdminSetting.current.invite_from_queue_number) %>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completely optional, nitpick: It would be nice if this also used count as the interpolation variable and had the one/other distinction in the locale.

<% end %>
</p>
<!--/descriptions-->
Expand All @@ -17,7 +17,9 @@
<p>
<%= label_tag :email %>
<%= text_field_tag :email %>
<%= submit_tag ts("Look me up") %>
<span class="submit actions">
<%= submit_tag t(".search") %>
</span>
</p>
</fieldset>
<% end %>
Expand Down
2 changes: 2 additions & 0 deletions config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,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 @@ -72,6 +72,11 @@ en:
external_works:
update:
successfully_updated: External work was successfully updated.
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 @@ -124,6 +124,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
28 changes: 28 additions & 0 deletions config/locales/views/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -565,10 +565,38 @@ 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_html:
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.
status:
heading: Invitation Request Status
search: Look me up
send_rate: We are sending out %{invites} invitations per day.
waiting_list:
one: There is currently %{count} person on the waiting list.
other: There are currently %{count} people on the waiting list.
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 @@ -80,6 +80,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
7 changes: 7 additions & 0 deletions db/migrate/20240415202120_add_index_to_invitations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddIndexToInvitations < ActiveRecord::Migration[6.1]
uses_departure! if Rails.env.staging? || Rails.env.production?

def change
add_index :invitations, :invitee_email
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]
Loading
Loading