Skip to content

Commit

Permalink
[DPC-4402] Add Expired Invitations list to Organization view for AO (#…
Browse files Browse the repository at this point in the history
…2333)

## 🎫 Ticket

https://jira.cms.gov/browse/DPC-4402

## 🛠 Changes

- Added a table to the list view component for expired invitations
- Query for expired invitations during OrganizationsController#show
action
- Filter out expired invitations from the pending invitations query in
OrganizationsController#show action

## ℹ️ Context

<!-- Why were these changes made? Add background context suitable for a
non-technical audience. -->
We want to render a list of expired invitations, so that the AO can keep
track and initiate a new invitation if need be.

<!-- If any of the following security implications apply, this PR must
not be merged without Stephen Walter's approval. Explain in this section
and add @SJWalter11 as a reviewer.
  - Adds a new software dependency or dependencies.
  - Modifies or invalidates one or more of our security controls.
  - Stores or transmits data that was not stored or transmitted before.
- Requires additional review of security implications for other reasons.
-->

## 🧪 Validation

<!-- How were the changes verified? Did you fully test the acceptance
criteria in the ticket? Provide reproducible testing instructions and
screenshots if applicable. -->
- [ ] Unit testing
- [ ] Manual verification -- see screenshot below

<img width="979" alt="Screenshot 2024-11-21 at 12 10 46 PM"
src="https://github.com/user-attachments/assets/b7a04198-26c0-4b68-8b38-692fd099c7fc">
  • Loading branch information
chris-ronning-ny authored Nov 22, 2024
1 parent 52df5e8 commit 8bf8187
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,18 @@
<p>There are no pending credential delegates.</p>
<% end %>
</div>
<div>
<h2>Expired invitations</h2>
<p>These invites expired. You can resend the invite to give them more time to accept.</p>
<% if @expired_invitations.present? %>
<%= render(Core::Table::TableComponent.new(id: 'expired-invitation-table', additional_classes: ['width-full'], sortable: false)) do %>
<%= render(Core::Table::HeaderComponent.new(caption: 'Expired Invitation Table',
columns: ['Name', 'Email', 'Expired on'])) %>
<%= render(Core::Table::RowComponent.with_collection(@expired_invitations, keys: ['full_name', 'email', 'expired_at'], obj_name: 'expired invitation')) %>
<% end %>
<% else %>
<p>You have no expired invitations.</p>
<% end %>
</div>
<% end %>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ module CredentialDelegate
class ListComponent < ViewComponent::Base
attr_reader :organization, :active_credential_delegates, :pending_credential_delegates

def initialize(organization, cd_invitations, credential_delegates)
def initialize(organization, pending_invitations, expired_invitations, credential_delegates)
super
@organization = organization
@active_credential_delegates = credential_delegates.map(&:show_attributes)
@pending_credential_delegates = cd_invitations.map(&:show_attributes)
@pending_credential_delegates = pending_invitations.map(&:show_attributes)
@expired_invitations = expired_invitations.map(&:show_attributes)
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,34 @@ module CredentialDelegate
#
class ListComponentPreview < ViewComponent::Preview
def empty
render(Page::CredentialDelegate::ListComponent.new(org, [], []))
render(Page::CredentialDelegate::ListComponent.new(org, [], [], []))
end

def active
user1 = User.new(given_name: 'Bob', family_name: 'Hodges', email: 'bob@example.com')
user2 = User.new(given_name: 'Lisa', family_name: 'Franklin', email: 'lisa@example.com')
cds = [CdOrgLink.new(user: user1, created_at: 1.week.ago), CdOrgLink.new(user: user2, created_at: 2.weeks.ago)]
render(Page::CredentialDelegate::ListComponent.new(org, [], cds))
render(Page::CredentialDelegate::ListComponent.new(org, [], [], cds))
end

def pending
cds = [
Invitation.new(invited_given_name: 'Bob', invited_family_name: 'Hodges', invited_email: 'bob@example.com',
id: 2),
id: 2, created_at: 1.day.ago),
Invitation.new(invited_given_name: 'Lisa', invited_family_name: 'Franklin',
invited_email: 'lisa@example.com', id: 3)
invited_email: 'lisa@example.com', id: 3, created_at: 1.day.ago)
]
render(Page::CredentialDelegate::ListComponent.new(org, cds, []))
render(Page::CredentialDelegate::ListComponent.new(org, cds, [], []))
end

def expired
expired_invites = [
Invitation.new(invited_given_name: 'Bob', invited_family_name: 'Hodges', invited_email: 'bob@example.com',
id: 2, created_at: 3.days.ago),
Invitation.new(invited_given_name: 'Lisa', invited_family_name: 'Franklin',
invited_email: 'lisa@example.com', id: 3, created_at: 3.days.ago)
]
render(Page::CredentialDelegate::ListComponent.new(org, [], expired_invites, []))
end

private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
<h1><%= @organization.name %></h1>
<div class="margin-bottom-5">NPI: <%= @organization.npi %></div>
<%= render(Core::Navigation::TabbedComponent.new('organization_nav', @links)) if @show_cds %>
<%= render(Page::CredentialDelegate::ListComponent.new(@organization, @active_credential_delegates, @pending_credential_delegates)) if @show_cds %>
<%= render(Page::CredentialDelegate::ListComponent.new(@organization, @pending_credential_delegates, @expired_cd_invitations, @active_credential_delegates,)) if @show_cds %>
<%= render(Page::Organization::CredentialsComponent.new(@organization)) %>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ module Page
module Organization
# Shows tabbed credential delegates and credentials
class CompoundShowComponent < ViewComponent::Base
def initialize(organization, cd_invitations, credential_delegates, show_cds)
def initialize(organization, cd_invitations, expired_cd_invitations, credential_delegates, show_cds)
super
@links = [['User Access', '#credential_delegates', true],
['Credentials', '#credentials', false]]
@organization = organization
@active_credential_delegates = credential_delegates
@pending_credential_delegates = cd_invitations
@expired_cd_invitations = expired_cd_invitations
@show_cds = show_cds
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ module Organization
class CompoundShowComponentPreview < ViewComponent::Preview
def authorized_official
org = ProviderOrganization.new(name: 'Health Hut', npi: '1111111111', id: 2)
render(Page::Organization::CompoundShowComponent.new(org, [], [], true))
render(Page::Organization::CompoundShowComponent.new(org, [], [], [], true))
end

def credential_delegate
org = ProviderOrganization.new(name: 'Health Hut', npi: '1111111111', id: 2)
render(Page::Organization::CompoundShowComponent.new(org, [], [], false))
render(Page::Organization::CompoundShowComponent.new(org, [], [], [], false))
end
end
end
Expand Down
13 changes: 9 additions & 4 deletions dpc-portal/app/controllers/organizations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@ def index
def show
show_cds = current_user.ao?(@organization)
if show_cds
@invitations = Invitation.where(provider_organization: @organization,
invited_by: current_user,
status: :pending)
# Invitation expiration is determined in relation to the `created_at` field; the `status` field will
# never be `'expired'`. Therefore, we need to further filter out expired invitations from this query.
@pending_invitations = Invitation.where(provider_organization: @organization,
invited_by: current_user,
status: :pending).reject(&:expired?)
@expired_invitations = Invitation.where(provider_organization: @organization,
invited_by: current_user).select(&:expired?)
@cds = CdOrgLink.where(provider_organization: @organization, disabled_at: nil)
end
render(Page::Organization::CompoundShowComponent.new(@organization, @cds, @invitations, show_cds))
render(Page::Organization::CompoundShowComponent.new(@organization, @pending_invitations, @expired_invitations,
@cds, show_cds))
end

def new
Expand Down
13 changes: 12 additions & 1 deletion dpc-portal/app/models/invitation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Invitation < ApplicationRecord
def show_attributes
{ full_name: "#{invited_given_name} #{invited_family_name}",
email: invited_email,
expired_at: expired_at.to_s,
id: }.with_indifferent_access
end

Expand All @@ -30,7 +31,17 @@ def invited_by_full_name
end

def expired?
created_at < 2.days.ago
Time.now > expiration_date
end

def expired_at
return unless expired?

expiration_date
end

def expiration_date
created_at + 2.days
end

def accept!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@

let(:org) { ComponentSupport::MockOrg.new }

let(:component) { described_class.new(org, invitations, credential_delegates) }
let(:component) { described_class.new(org, pending_invitations, expired_invitations, credential_delegates) }

before do
render_inline(component)
end

context 'No credential delegates' do
let(:invitations) { [] }
let(:pending_invitations) { [] }
let(:expired_invitations) { [] }
let(:credential_delegates) { [] }

let(:expected_html) do
Expand All @@ -45,6 +46,11 @@
<h2>Pending invitations</h2>
<p>There are no pending credential delegates.</p>
</div>
<div>
<h2>Expired invitations</h2>
<p>These invites expired. You can resend the invite to give them more time to accept.</p>
<p>You have no expired invitations.</p>
</div>
</div>
</div>
</div>
Expand All @@ -59,7 +65,8 @@
let(:invitation) do
Invitation.new(invited_given_name: 'Bob', invited_family_name: 'Hodges', invited_email: 'bob@example.com')
end
let(:invitations) { [] }
let(:pending_invitations) { [] }
let(:expired_invitations) { [] }
let(:credential_delegates) { [CdOrgLink.new(user:, invitation:)] }

it 'has a table' do
Expand Down Expand Up @@ -102,10 +109,11 @@
end

context 'Pending credential delegate' do
let(:invitations) do
let(:pending_invitations) do
[Invitation.new(invited_given_name: 'Bob', invited_family_name: 'Hodges', invited_email: 'bob@example.com',
id: 3)]
id: 3, created_at: 1.day.ago)]
end
let(:expired_invitations) { [] }
let(:credential_delegates) { [] }

it 'has a table' do
Expand Down Expand Up @@ -148,6 +156,61 @@
expected_html = '<p>There are no active credential delegates.</p>'
is_expected.to include(normalize_space(expected_html))
end

it 'has no expired invitations' do
expected_html = '<p>You have no expired invitations.</p>'
is_expected.to include(normalize_space(expected_html))
end
end

context 'Expired invitations' do
let(:pending_invitations) { [] }
let(:expired_invitations) do
[Invitation.new(invited_given_name: 'Bob', invited_family_name: 'Hodges', invited_email: 'bob@example.com',
id: 3, created_at: 3.days.ago)]
end
let(:credential_delegates) { [] }

it 'has a table' do
expired_at = expired_invitations.first.expired_at
expected_html = <<~HTML
<table id="expired-invitation-table" class="width-full usa-table">
<caption aria-hidden="true" hidden>Expired Invitation Table</caption>
<thead>
<tr>
<th scope="row" role="columnheader">Name</th>
<th scope="row" role="columnheader">Email</th>
<th scope="row" role="columnheader">Expired on</th>
</tr>
</thead>
<tbody>
<tr>
<td data-sort-value="Bob Hodges">Bob Hodges</td>
<td data-sort-value="bob@example.com">bob@example.com</td>
<td data-sort-value="#{expired_at}">#{expired_at}</td>
</tr>
</tbody>
</table>
HTML
is_expected.to include(normalize_space(expected_html))
end

it 'has a row' do
expired_at = expired_invitations.first.expired_at
expected_html = <<~HTML
<tr>
<td data-sort-value="Bob Hodges">Bob Hodges</td>
<td data-sort-value="bob@example.com">bob@example.com</td>
<td data-sort-value="#{expired_at}">#{expired_at}</td>
</tr>
HTML
is_expected.to include(normalize_space(expected_html))
end

it 'has no pending credential delegates' do
expected_html = '<p>There are no pending credential delegates.</p>'
is_expected.to include(normalize_space(expected_html))
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
normalize_space(rendered_content)
end
let(:org) { build(:provider_organization, name: 'Health Hut', npi: '11111111', id: 2) }
let(:component) { described_class.new(org, [], [], show_cds) }
let(:component) { described_class.new(org, [], [], [], show_cds) }

before do
render_inline(component)
Expand Down
36 changes: 31 additions & 5 deletions dpc-portal/spec/requests/organizations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@
it 'does not assign invitations even if exist' do
create(:invitation, :cd, provider_organization: org, invited_by: user)
get "/organizations/#{org.id}"
expect(assigns(:invitations)).to be_nil
expect(assigns(:pending_invitations)).to be_nil
end
end
end
Expand Down Expand Up @@ -260,24 +260,50 @@
expect(response.body).to include('<h2>Credential delegates</h2>')
expect(response.body).to include('<h2>Pending invitations</h2>')
expect(response.body).to include('<h2>Active</h2>')
expect(response.body).to include('<h2>Expired invitations</h2>')
end

context :invitations do
context :pending_invitations do
it 'assigns if exist' do
create(:invitation, :cd, provider_organization: org, invited_by: user)
get "/organizations/#{org.id}"
expect(assigns(:invitations).size).to eq 1
expect(assigns(:pending_invitations).size).to eq 1
end

it 'does not assign if not exist' do
get "/organizations/#{org.id}"
expect(assigns(:invitations).size).to eq 0
expect(assigns(:pending_invitations).size).to eq 0
end

it 'does not assign if only accepted exists' do
create(:invitation, :cd, provider_organization: org, invited_by: user, status: :accepted)
get "/organizations/#{org.id}"
expect(assigns(:invitations).size).to eq 0
expect(assigns(:pending_invitations).size).to eq 0
end

it 'does not assign if expired' do
create(:invitation, :cd, provider_organization: org, invited_by: user, created_at: 3.days.ago)
get "/organizations/#{org.id}"
expect(assigns(:pending_invitations).size).to eq 0
end
end

context :expired_invitations do
it 'assigns if exist' do
create(:invitation, :cd, provider_organization: org, invited_by: user, created_at: 3.days.ago)
get "/organizations/#{org.id}"
expect(assigns(:expired_invitations).size).to eq 1
end

it 'does not assign if not exist' do
get "/organizations/#{org.id}"
expect(assigns(:pending_invitations).size).to eq 0
end

it 'does not assign if invitation is not expired' do
create(:invitation, :cd, provider_organization: org, invited_by: user)
get "/organizations/#{org.id}"
expect(assigns(:expired_invitations).size).to eq 0
end
end

Expand Down

0 comments on commit 8bf8187

Please sign in to comment.