Allow admins to delete an organization (#19699)
* feat: update the layout for organizations * feat: update the layout for an organization * feat: organization logos are square * feat: change the text on the org visit button * feat: initialize the dropdown menu * feat: trigger the modal * feat: update the text in the model * feat: tweak wording * WIP/feat: hook up a route with an action that calls an organization delete worker. * feat: redirect after showing the notice * feat: update the authorization required to be super admin * feat: add the notice to localization * spec: integration test * spec: fix * spec: delete worker * chore: update the link * feat: add safe navigation * feat: make sure that we show an error if the organization has credits * fix: error message for modal unspent credits * feat add the spec for the helper * fix: remove rspec preface * fix: small fixes to tests and I18n * fix: updated the number of orgs * feat: update the message * feat: update the message * chore: remove inclusion of helper * feat: change the name of the parameter * chore: newline
This commit is contained in:
parent
20c282df80
commit
272aea6127
20 changed files with 415 additions and 93 deletions
|
|
@ -29,6 +29,18 @@ module Admin
|
|||
redirect_to admin_organization_path(org)
|
||||
end
|
||||
|
||||
def destroy
|
||||
organization = Organization.find_by(id: params[:id])
|
||||
Organizations::DeleteWorker.perform_async(organization.id, current_user.id, false)
|
||||
|
||||
flash[:settings_notice] =
|
||||
I18n.t("admin.organizations_controller.deletion_scheduled", organization_name: organization.name)
|
||||
redirect_to admin_organization_url(params[:id])
|
||||
rescue StandardError => e
|
||||
flash[:error] = I18n.t("admin.organizations_controller.error", organization_name: organization.name, error: e)
|
||||
redirect_to user_settings_path(:organization, id: organization.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_note(org)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,6 @@ class BillboardsController < ApplicationController
|
|||
def user_tags
|
||||
return unless feed_targeted_tag_placement?(placement_area)
|
||||
|
||||
current_user.cached_followed_tag_names
|
||||
current_user&.cached_followed_tag_names
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class OrganizationsController < ApplicationController
|
|||
organization = Organization.find_by(id: params[:id])
|
||||
authorize organization
|
||||
|
||||
Organizations::DeleteWorker.perform_async(organization.id, current_user.id)
|
||||
Organizations::DeleteWorker.perform_async(organization.id, current_user.id, true)
|
||||
flash[:settings_notice] =
|
||||
I18n.t("organizations_controller.deletion_scheduled", organization_name: organization.name)
|
||||
|
||||
|
|
|
|||
16
app/helpers/admin/organizations_helper.rb
Normal file
16
app/helpers/admin/organizations_helper.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
module Admin
|
||||
module OrganizationsHelper
|
||||
def deletion_modal_error_message(organization)
|
||||
error_message = nil
|
||||
unless current_user.super_admin?
|
||||
error_message = I18n.t("views.admin.organizations.delete.role_notice")
|
||||
end
|
||||
|
||||
if organization.credits.length.positive?
|
||||
error_message = "#{error_message} #{I18n.t('views.admin.organizations.delete.credits_notice')}"
|
||||
end
|
||||
|
||||
error_message&.strip
|
||||
end
|
||||
end
|
||||
end
|
||||
9
app/javascript/packs/admin/organizations.jsx
Normal file
9
app/javascript/packs/admin/organizations.jsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { showOrganizationModal } from '././organizations/modals';
|
||||
import { initializeDropdown } from '@utilities/dropdownUtils';
|
||||
|
||||
initializeDropdown({
|
||||
triggerElementId: 'options-dropdown-trigger',
|
||||
dropdownContentId: 'options-dropdown',
|
||||
});
|
||||
|
||||
document.body.addEventListener('click', showOrganizationModal);
|
||||
45
app/javascript/packs/admin/organizations/modals.js
Normal file
45
app/javascript/packs/admin/organizations/modals.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { showWindowModal } from '@utilities/showModal';
|
||||
|
||||
const modalContents = new Map();
|
||||
/**
|
||||
* Helper function to handle finding and caching modal content. Since our Preact modal helper works by duplicating HTML content,
|
||||
* and our user modals rely on IDs to label form controls, we remove the original hidden content from the DOM to avoid ID conflicts.
|
||||
*
|
||||
* @param {string} modalContentSelector The CSS selector used to identify the correct modal content
|
||||
*/
|
||||
const getModalContents = (modalContentSelector) => {
|
||||
if (!modalContents.has(modalContentSelector)) {
|
||||
const modalContentElement = document.querySelector(modalContentSelector);
|
||||
const modalContent = modalContentElement.innerHTML;
|
||||
|
||||
modalContentElement.remove();
|
||||
modalContents.set(modalContentSelector, modalContent);
|
||||
}
|
||||
|
||||
return modalContents.get(modalContentSelector);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function for views which use admin user modals. May be attached as an event listener, and its actions will only be triggered
|
||||
* if the target of the event is a recognised user modal trigger.
|
||||
*
|
||||
* @param {Object} event
|
||||
*/
|
||||
export const showOrganizationModal = (event) => {
|
||||
const { dataset } = event.target;
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(dataset, 'modalContentSelector')) {
|
||||
// We're not trying to trigger a modal.
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const { modalTitle, modalSize, modalContentSelector } = dataset;
|
||||
|
||||
showWindowModal({
|
||||
modalContent: getModalContents(modalContentSelector),
|
||||
title: modalTitle,
|
||||
size: modalSize,
|
||||
});
|
||||
};
|
||||
|
|
@ -1,48 +1,86 @@
|
|||
<header class="flex items-center mb-6">
|
||||
<div>
|
||||
<h1 class="crayons-title"><%= @organization.name %></h1>
|
||||
<p class="color-base-60">Created <%= @organization.created_at.strftime("%b %e '%y") %></p>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<%= link_to "View @#{@organization.name}", "/#{@organization.slug}", class: "crayons-btn", target: "_blank", rel: "noopener" %>
|
||||
</div>
|
||||
<%= javascript_packs_with_chunks_tag "admin/organizations", defer: true %>
|
||||
<header>
|
||||
<section class="crayons-card flex flex-col l:flex-row gap-2 m:gap-4 l:gap-6 mb-2 m:mb-3 p-3 s:p-4 m:p-7">
|
||||
<span class="crayons-logo crayons-logo--3xl">
|
||||
<img src="<%= @organization.profile_image %>" width="100%" height="100%" alt="<%= @organization.name %> profile picture">
|
||||
</span>
|
||||
<div class="grid gap-2 flex-1">
|
||||
<div class="s:flex items-center gap-5">
|
||||
<div class="flex flex-1 items-center">
|
||||
<h1 class="crayons-title lh-tight">
|
||||
<%= @organization.name %>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex relative justify-between s:justify-end gap-2 my-2 s:my-0">
|
||||
<%= link_to t("views.admin.organizations.profile.visit"), "/#{@organization.slug}", class: "c-cta", target: "_blank", rel: "noopener" %>
|
||||
<div class="dropdown-trigger-container">
|
||||
<button type="button" class="c-btn c-btn--icon-alone dropdown-trigger" id="options-dropdown-trigger" aria-haspopup="true" aria-expanded="false" aria-controls="options-dropdown">
|
||||
<%= crayons_icon_tag("overflow-vertical", title: t("views.admin.users.profile.options.icon")) %>
|
||||
</button>
|
||||
<div class="crayons-dropdown right-0 left-0 s:left-auto" id="options-dropdown">
|
||||
<ul class="p-0">
|
||||
<li>
|
||||
<button
|
||||
type="button" class="c-btn c-btn--destructive w-100 align-left"
|
||||
data-modal-title="<%= t("views.admin.organizations.delete.heading", organization: @organization.name) %>"
|
||||
data-modal-size="small"
|
||||
data-modal-content-selector="#delete-organization">
|
||||
<%= t("views.admin.organizations.options.delete") %>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fs-s mb-1 color-secondary">
|
||||
<%= @organization.slug %> <span class="opacity-50">•</span> <%= t("views.admin.organizations.profile.id", organizationid: @organization.id) %> <span class="opacity-50">•</span>
|
||||
<%= t("views.admin.organizations.profile.since_html", time: tag.time(l(@organization.created_at, format: :short_with_yy), datetime: @organization.created_at.strftime("%Y-%m-%dT%H:%M:%S%z"), title: l(@organization.created_at, format: :admin_user))) %>
|
||||
<span class="opacity-50">•</span> <%= t("views.admin.organizations.profile.total_members", count: @organization.organization_memberships.size) %>
|
||||
</div>
|
||||
<div>
|
||||
<ul class="flex flex-col s:flex-row gap-3 s:gap-6 flex-wrap">
|
||||
<li>
|
||||
<% if @organization.email %>
|
||||
<a href="mailto:<%= @organization.email %>" class="c-link c-link--icon-left inline-block" target="_blank" rel="noopener">
|
||||
<%= crayons_icon_tag(:email, title: t("views.admin.organizations.profile.email"), class: "c-link__icon") %>
|
||||
<%= @organization.email %>
|
||||
</a>
|
||||
<% else %>
|
||||
<%= crayons_icon_tag(:email, title: t("views.admin.organizations.profile.email"), class: "c-link__icon") %>
|
||||
N/A
|
||||
<% end %>
|
||||
</li>
|
||||
<% if @organization.github_username %>
|
||||
<li>
|
||||
<a href="https://github.com/<%= @organization.github_username %>" class="c-link c-link--icon-left inline-block" target="_blank">
|
||||
<%= crayons_icon_tag(:github, title: t("views.admin.users.profile.github")) %>
|
||||
<%= @organization.github_username %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if @organization.twitter_username %>
|
||||
<li>
|
||||
<a href="https://twitter.com/<%= @organization.twitter_username %>" class="c-link c-link--icon-left inline-block" target="_blank">
|
||||
<%= crayons_icon_tag(:twitter, title: t("views.admin.users.profile.twitter")) %>
|
||||
<%= @organization.twitter_username %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
<div class="crayons-card p-6 mb-6">
|
||||
<h3 class="crayons-subtitle-2 mb-4">General Info</h3>
|
||||
<dl>
|
||||
<dt>ID:</dt>
|
||||
<dd><%= @organization.id %></dd>
|
||||
<dt>Name:</dt>
|
||||
<dd><%= @organization.name %></dd>
|
||||
<dt>Membership Count:</dt>
|
||||
<dd><%= @organization.organization_memberships.size %></dd>
|
||||
<dt>Email:</dt>
|
||||
<dd><%= @organization.email || "N/A" %></dd>
|
||||
<dt>Twitter:</dt>
|
||||
<% if @organization.twitter_username %>
|
||||
<dd><%= link_to @organization.twitter_username, "https://twitter.com/#{@organization.twitter_username}" %></dd>
|
||||
<% else %>
|
||||
<dd>N/A</dd>
|
||||
<% end %>
|
||||
<dt>GitHub:</dt>
|
||||
<% if @organization.github_username %>
|
||||
<dd><%= link_to @organization.github_username, "https://github.com/#{@organization.github_username}" %></dd>
|
||||
<% else %>
|
||||
<dd>N/A</dd>
|
||||
<% end %>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="crayons-card p-6 mb-6">
|
||||
<% current_credits = @organization.unspent_credits_count %>
|
||||
<h3 class="crayons-subtitle-2 mb-4">Credits (current: <%= current_credits %>)</h3>
|
||||
<%= form_tag update_org_credits_admin_organization_path(@organization), method: :patch, class: "flex justify-between mb-2" do %>
|
||||
<div>
|
||||
<%= hidden_field_tag :credit_action, :add %>
|
||||
<%= number_field_tag :credits, nil, in: 1...100_000, required: true, class: "crayons-textfield w-auto mr-3", size: 5, aria: { label: "Credits" } %>
|
||||
<%= text_field_tag :note, "", placeholder: "Why are you adding these credits?", size: 50, required: true, class: "crayons-textfield w-auto mr-3", aria: { label: "Reason" } %>
|
||||
<%= number_field_tag :credits, nil, in: 1...100_000, required: true, class: "crayons-textfield w-auto mr-3", size: 5, aria: { label: "Credits" }, autocomplete: "off" %>
|
||||
<%= text_field_tag :note, "", placeholder: "Why are you adding these credits?", size: 50, required: true, class: "crayons-textfield w-auto mr-3", aria: { label: "Reason" }, autocomplete: "off" %>
|
||||
</div>
|
||||
<%= submit_tag "Add Org Credits", class: "crayons-btn" %>
|
||||
<% end %>
|
||||
|
|
@ -50,8 +88,8 @@
|
|||
<%= form_tag update_org_credits_admin_organization_path(@organization), method: :patch, class: "flex justify-between mb-2" do %>
|
||||
<div>
|
||||
<%= hidden_field_tag :credit_action, :remove %>
|
||||
<%= number_field_tag :credits, nil, in: 1..current_credits, required: true, class: "crayons-textfield w-auto mr-3", size: 5, aria: { label: "Credits" } %>
|
||||
<%= text_field_tag :note, "", placeholder: "Why are you removing these credits?", size: 50, required: true, class: "crayons-textfield w-auto mr-3", aria: { label: "Reason" } %>
|
||||
<%= number_field_tag :credits, nil, in: 1..current_credits, required: true, class: "crayons-textfield w-auto mr-3", size: 5, aria: { label: "Credits" }, autocomplete: "off" %>
|
||||
<%= text_field_tag :note, "", placeholder: "Why are you removing these credits?", size: 50, required: true, class: "crayons-textfield w-auto mr-3", aria: { label: "Reason" }, autocomplete: "off" %>
|
||||
</div>
|
||||
<%= submit_tag "Remove Org Credits", class: "crayons-btn crayons-btn--danger" %>
|
||||
<% end %>
|
||||
|
|
@ -59,3 +97,19 @@
|
|||
</div>
|
||||
|
||||
<%= render "activity" %>
|
||||
|
||||
<% error_message = deletion_modal_error_message(@organization) %>
|
||||
<div class="hidden">
|
||||
<div id="delete-organization">
|
||||
<% if error_message %>
|
||||
<div class="crayons-notice crayons-notice--danger"><%= error_message %></div>
|
||||
<% else %>
|
||||
<%= form_for(@organization, url: admin_organization_path(@organization), html: { class: "flex flex-col gap-6", method: :delete, onsubmit: "return confirm('#{j t('views.admin.organizations.delete.onsubmit')}')", id: nil }) do |f| %>
|
||||
<%= t("views.admin.organizations.delete.desc2", organization: @organization.name) %>
|
||||
<div>
|
||||
<button class="c-btn c-btn--primary c-btn--destructive"><%= t("views.admin.organizations.delete.submit") %></button>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ module Organizations
|
|||
sidekiq_options queue: :high_priority, retry: 10
|
||||
|
||||
# operator_id - the user who deletes the organization
|
||||
def perform(organization_id, operator_id)
|
||||
# deleted_by_org_admin - the organization can be deleted by either
|
||||
# the admin of an organization or by the Forem Admin.
|
||||
def perform(organization_id, operator_id, deleted_by_org_admin)
|
||||
org = Organization.find_by(id: organization_id)
|
||||
return unless org
|
||||
|
||||
|
|
@ -14,11 +16,12 @@ module Organizations
|
|||
|
||||
Organizations::Delete.call(org)
|
||||
|
||||
user.touch(:organization_info_updated_at)
|
||||
EdgeCache::BustUser.call(user)
|
||||
if deleted_by_org_admin
|
||||
user.touch(:organization_info_updated_at)
|
||||
EdgeCache::BustUser.call(user)
|
||||
|
||||
# notify user that the org was deleted
|
||||
NotifyMailer.with(name: user.name, org_name: org.name, email: user.email).organization_deleted_email.deliver_now
|
||||
NotifyMailer.with(name: user.name, org_name: org.name, email: user.email).organization_deleted_email.deliver_now
|
||||
end
|
||||
|
||||
audit_log(org, user)
|
||||
rescue StandardError => e
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ en:
|
|||
updated: User was successfully updated to %{type}
|
||||
organizations_controller:
|
||||
credit_updated: Successfully updated credits
|
||||
deletion_scheduled: 'Organization, "%{organization_name}", deletion is scheduled.'
|
||||
error: 'Your organization, %{organization_name}, could not be deleted: %{error}.'
|
||||
pages_controller:
|
||||
updated: Page has been successfully updated.
|
||||
created: Page has been successfully created.
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ fr:
|
|||
updated: L'utilisateur a été mis à jour avec succès vers %{type}
|
||||
organizations_controller:
|
||||
credit_updated: Crédits actualisés avec succès
|
||||
deletion_scheduled: 'Organization, "%{organization_name}", deletion is scheduled.'
|
||||
error: 'Your organization, %{organization_name}, could not be deleted: %{error}.'
|
||||
pages_controller:
|
||||
updated: La page a été mise à jour avec succès.
|
||||
created: La page a été créée avec succès.
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ en:
|
|||
submit_html: Banish %{user}
|
||||
delete:
|
||||
heading: Delete %{user}'s account
|
||||
onsubmit: Are you sure? This actions is irreversible.
|
||||
onsubmit: Are you sure? This action is irreversible.
|
||||
desc2: Once deleted, all data relating to %{user} will be removed from our database.
|
||||
notice: Only Super Admins are allowed to delete users.
|
||||
submit: Delete now
|
||||
|
|
@ -389,3 +389,22 @@ en:
|
|||
facebook: Facebook
|
||||
github: GitHub
|
||||
twitter: Twitter
|
||||
organizations:
|
||||
profile:
|
||||
id: ID %{organizationid}
|
||||
since_html: Organization since %{time}
|
||||
email: Email
|
||||
github: GitHub
|
||||
twitter: Twitter
|
||||
visit: Visit organization
|
||||
total_members: "%{count} members"
|
||||
delete:
|
||||
heading: Delete %{organization}
|
||||
onsubmit: Are you sure? This action is irreversible.
|
||||
desc2: "Note: Deleting organization, %{organization}, will dissasociate the existing members and articles from the organization but will not delete them."
|
||||
role_notice: Only Super Admins are allowed to delete organizations.
|
||||
credits_notice: You cannot delete an organization that has associated credits.
|
||||
submit: Delete organizion now
|
||||
options:
|
||||
delete: Delete organization
|
||||
icon: Options
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ fr:
|
|||
description: Toutes les actions du modérateur affectent le score. Le score global est une combinaison des actions des modérateurs et des réactions du public.
|
||||
no_flags: Le commentaire n'a pas de drapeaux.
|
||||
no_quality_reactions: Le commentaire n'a pas de réactions de qualité par des utilisateurs de confiance.
|
||||
title: Actions du modérateur
|
||||
title: Actions du modérateur
|
||||
shared:
|
||||
flags:
|
||||
actions:
|
||||
|
|
@ -147,7 +147,7 @@ fr:
|
|||
submit_html: Banish %{user}
|
||||
delete:
|
||||
heading: Delete %{user}'s account
|
||||
onsubmit: Are you sure? This actions is irreversible.
|
||||
onsubmit: Are you sure? This action is irreversible.
|
||||
desc2: Once deleted, all data relating to %{user} will be removed from our database.
|
||||
notice: Only Super Admins are allowed to delete users.
|
||||
submit: Delete now
|
||||
|
|
@ -389,3 +389,22 @@ fr:
|
|||
facebook: Facebook
|
||||
github: GitHub
|
||||
twitter: Twitter
|
||||
organizations:
|
||||
profile:
|
||||
id: ID %{organizationid}
|
||||
since_html: Organization since %{time}
|
||||
email: Email
|
||||
github: GitHub
|
||||
twitter: Twitter
|
||||
visit: Visit organization
|
||||
total_members: "%{count} members"
|
||||
delete:
|
||||
heading: Delete %{organization}
|
||||
onsubmit: Are you sure? This action is irreversible.
|
||||
desc2: "Note: Deleting organization, %{organization}, will dissasociate the existing members and articles from the organization but will not delete them."
|
||||
role_notice: Only Super Admins are allowed to delete organizations.
|
||||
credits_notice: You cannot delete an organization that has associated credits.
|
||||
submit: Delete organizion now
|
||||
options:
|
||||
delete: Delete organization
|
||||
icon: Options
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ namespace :admin do
|
|||
get "/badge_achievements/award_badges", to: "badge_achievements#award"
|
||||
post "/badge_achievements/award_badges", to: "badge_achievements#award_badges"
|
||||
resources :comments, only: %i[index show]
|
||||
resources :organizations, only: %i[index show] do
|
||||
resources :organizations, only: %i[index show destroy] do
|
||||
member do
|
||||
patch "update_org_credits"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
import { verifyAndDismissFlashMessage } from '../shared/adminUtilities';
|
||||
|
||||
function openOrganizationOptions(callback) {
|
||||
cy.findByRole('button', { name: 'Options' })
|
||||
.should('have.attr', 'aria-haspopup', 'true')
|
||||
.should('have.attr', 'aria-expanded', 'false')
|
||||
.click()
|
||||
.then(([button]) => {
|
||||
expect(button.getAttribute('aria-expanded')).to.equal('true');
|
||||
const dropdownId = button.getAttribute('aria-controls');
|
||||
|
||||
cy.get(`#${dropdownId}`).within(callback);
|
||||
});
|
||||
}
|
||||
|
||||
describe('Manage Organization Options', () => {
|
||||
describe('As a super admin', () => {
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.fixture('users/adminUser.json').as('user');
|
||||
cy.get('@user').then((user) => {
|
||||
cy.loginAndVisit(user, '/admin/content_manager/organizations');
|
||||
});
|
||||
|
||||
cy.findByRole('table').within(() => {
|
||||
cy.findAllByRole('link', { name: '@Awesome Org' }).first().click();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show a notice about the scheduled job', () => {
|
||||
openOrganizationOptions(() => {
|
||||
cy.findByRole('button', { name: 'Delete organization' }).click();
|
||||
});
|
||||
|
||||
cy.getModal().within(() => {
|
||||
cy.findByRole('button', { name: 'Delete organizion now' }).click();
|
||||
});
|
||||
|
||||
verifyAndDismissFlashMessage(
|
||||
`Organization, "Awesome Org", deletion is scheduled.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('As an organization that has existing credits', () => {
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.fixture('users/adminUser.json').as('user');
|
||||
cy.get('@user').then((user) => {
|
||||
cy.loginAndVisit(user, '/admin/content_manager/organizations');
|
||||
});
|
||||
|
||||
cy.findByRole('table').within(() => {
|
||||
cy.findAllByRole('link', { name: '@Credits Org' }).first().click();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show an error', () => {
|
||||
openOrganizationOptions(() => {
|
||||
cy.findByRole('button', { name: 'Delete organization' }).click();
|
||||
});
|
||||
|
||||
cy.getModal().within(() => {
|
||||
cy.findByText(
|
||||
'You cannot delete an organization that has associated credits.',
|
||||
).should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
20
cypress/e2e/seededFlows/adminFlows/shared/adminUtilities.js
Normal file
20
cypress/e2e/seededFlows/adminFlows/shared/adminUtilities.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* E2E helper function for user admin tests that validates the correct flash notice message appears
|
||||
*
|
||||
* @param {string} [message] The expected flash message text
|
||||
*/
|
||||
export function verifyAndDismissFlashMessage(message) {
|
||||
cy.findByTestId('flash-settings_notice')
|
||||
.as('notice')
|
||||
.then((element) => {
|
||||
expect(element.text().trim()).equal(message);
|
||||
});
|
||||
|
||||
cy.get('@notice').within(() => {
|
||||
cy.findByRole('button', { name: 'Dismiss message' })
|
||||
.should('have.focus')
|
||||
.click();
|
||||
});
|
||||
|
||||
cy.findByTestId('flash-settings_notice').should('not.exist');
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ describe('User index view', () => {
|
|||
cy.findAllByText('Good standing').should('exist');
|
||||
cy.findByText('Last activity').should('exist');
|
||||
cy.findByText('Joined on').should('exist');
|
||||
cy.findByRole('figure').findByText('+ 1').should('exist');
|
||||
cy.findByRole('figure').findByText('+ 2').should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ describe('User index view', () => {
|
|||
);
|
||||
cy.findByAltText('Many orgs user').should('exist');
|
||||
cy.findAllByText('Good standing').should('exist');
|
||||
cy.findByRole('figure').findByText('+ 1').should('exist');
|
||||
cy.findByRole('figure').findByText('+ 2').should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
28
spec/helpers/admin/organizations_helper_spec.rb
Normal file
28
spec/helpers/admin/organizations_helper_spec.rb
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
require "rails_helper"
|
||||
|
||||
describe Admin::OrganizationsHelper do
|
||||
describe "#deletion_modal_error_message" do
|
||||
let(:organization) { create(:organization) }
|
||||
let(:super_admin) { create(:user, :super_admin) }
|
||||
let(:admin) { create(:user, :admin) }
|
||||
|
||||
context "when the current user is not a super admin" do
|
||||
before { allow(helper).to receive(:current_user).and_return(admin) }
|
||||
|
||||
it "returns the appropriate error messsage" do
|
||||
error_message = helper.deletion_modal_error_message(organization)
|
||||
expect(error_message).to eq("Only Super Admins are allowed to delete organizations.")
|
||||
end
|
||||
end
|
||||
|
||||
context "when there are credits associated to an organization" do
|
||||
before { allow(helper).to receive(:current_user).and_return(super_admin) }
|
||||
|
||||
it "returns the appropriate error messsage" do
|
||||
Credit.add_to(organization, 10)
|
||||
error_message = helper.deletion_modal_error_message(organization)
|
||||
expect(error_message).to eq("You cannot delete an organization that has associated credits.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -180,7 +180,7 @@ RSpec.describe "UserOrganization" do
|
|||
end
|
||||
|
||||
it "deletes the organization" do
|
||||
sidekiq_assert_enqueued_with(job: Organizations::DeleteWorker, args: [org_id, org_admin.id]) do
|
||||
sidekiq_assert_enqueued_with(job: Organizations::DeleteWorker, args: [org_id, org_admin.id, true]) do
|
||||
delete organization_path(org_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -303,6 +303,24 @@ seeder.create_if_doesnt_exist(Organization, "slug", "org4") do
|
|||
)
|
||||
end
|
||||
|
||||
seeder.create_if_doesnt_exist(Organization, "slug", "creditsorg") do
|
||||
organization = Organization.create!(
|
||||
name: "Credits Org",
|
||||
summary: Faker::Company.bs,
|
||||
profile_image: Rails.root.join("app/assets/images/#{rand(1..40)}.png").open,
|
||||
url: Faker::Internet.url,
|
||||
slug: "creditsorg",
|
||||
)
|
||||
|
||||
OrganizationMembership.create!(
|
||||
user_id: many_orgs_user.id,
|
||||
organization_id: organization.id,
|
||||
type_of_user: "member",
|
||||
)
|
||||
|
||||
Credit.add_to(organization, 100)
|
||||
end
|
||||
|
||||
##############################################################################
|
||||
|
||||
seeder.create_if_doesnt_exist(User, "email", "change-password-user@forem.com") do
|
||||
|
|
|
|||
|
|
@ -10,82 +10,87 @@ RSpec.describe Organizations::DeleteWorker, type: :worker do
|
|||
let(:mailer_class) { NotifyMailer }
|
||||
let(:mailer) { double }
|
||||
let(:message_delivery) { double }
|
||||
let(:update_and_notify_user) { true }
|
||||
|
||||
context "when org and user are found" do
|
||||
it "destroys the org" do
|
||||
worker.perform(org.id, user.id)
|
||||
worker.perform(org.id, user.id, update_and_notify_user)
|
||||
expect(Organization.exists?(id: org.id)).to be(false)
|
||||
end
|
||||
|
||||
it "calls the service" do
|
||||
allow(delete).to receive(:call)
|
||||
worker.perform(org.id, user.id)
|
||||
worker.perform(org.id, user.id, update_and_notify_user)
|
||||
expect(delete).to have_received(:call).with(org)
|
||||
end
|
||||
|
||||
it "touches the user" do
|
||||
allow(User).to receive(:find_by) { user }
|
||||
allow(user).to receive(:touch)
|
||||
worker.perform(org.id, user.id)
|
||||
expect(user).to have_received(:touch).with(:organization_info_updated_at)
|
||||
end
|
||||
|
||||
it "busts user's cache" do
|
||||
bust_cache = EdgeCache::BustUser
|
||||
allow(bust_cache).to receive(:call)
|
||||
worker.perform(org.id, user.id)
|
||||
expect(bust_cache).to have_received(:call).with(user)
|
||||
end
|
||||
|
||||
it "sends the notification" do
|
||||
allow(ForemInstance).to receive(:smtp_enabled?).and_return(true)
|
||||
expect do
|
||||
worker.perform(org.id, user.id)
|
||||
end.to change(ActionMailer::Base.deliveries, :count).by(1)
|
||||
end
|
||||
|
||||
it "sends the correct notification" do
|
||||
allow(mailer_class).to receive(:with).and_return(mailer)
|
||||
allow(mailer).to receive(:organization_deleted_email).and_return(message_delivery)
|
||||
allow(message_delivery).to receive(:deliver_now)
|
||||
|
||||
worker.perform(org.id, user.id)
|
||||
|
||||
expect(mailer_class).to have_received(:with).with(org_name: org.name, name: user.name, email: user.email)
|
||||
expect(mailer).to have_received(:organization_deleted_email)
|
||||
expect(message_delivery).to have_received(:deliver_now)
|
||||
end
|
||||
|
||||
it "creates an audit_log record" do
|
||||
expect do
|
||||
worker.perform(org.id, user.id)
|
||||
worker.perform(org.id, user.id, update_and_notify_user)
|
||||
end.to change(AuditLog, :count).by(1)
|
||||
end
|
||||
|
||||
it "creates a correct AuditLog record" do
|
||||
worker.perform(org.id, user.id)
|
||||
worker.perform(org.id, user.id, update_and_notify_user)
|
||||
audit_log = AuditLog.find_by(category: "user.organization.delete", slug: "organization_delete",
|
||||
user_id: user.id)
|
||||
expect(audit_log).to be_present
|
||||
expect(audit_log.data["organization_id"]).to eq(org.id)
|
||||
expect(audit_log.data["organization_slug"]).to eq(org.slug)
|
||||
end
|
||||
|
||||
# rubocop:disable RSpec/NestedGroups
|
||||
context "when update_and_notify_user is true" do
|
||||
it "touches the user" do
|
||||
allow(User).to receive(:find_by) { user }
|
||||
allow(user).to receive(:touch)
|
||||
worker.perform(org.id, user.id, update_and_notify_user)
|
||||
expect(user).to have_received(:touch).with(:organization_info_updated_at)
|
||||
end
|
||||
|
||||
it "busts user's cache" do
|
||||
bust_cache = EdgeCache::BustUser
|
||||
allow(bust_cache).to receive(:call)
|
||||
worker.perform(org.id, user.id, update_and_notify_user)
|
||||
expect(bust_cache).to have_received(:call).with(user)
|
||||
end
|
||||
|
||||
it "sends the notification" do
|
||||
allow(ForemInstance).to receive(:smtp_enabled?).and_return(true)
|
||||
expect do
|
||||
worker.perform(org.id, user.id, update_and_notify_user)
|
||||
end.to change(ActionMailer::Base.deliveries, :count).by(1)
|
||||
end
|
||||
|
||||
it "sends the correct notification" do
|
||||
allow(mailer_class).to receive(:with).and_return(mailer)
|
||||
allow(mailer).to receive(:organization_deleted_email).and_return(message_delivery)
|
||||
allow(message_delivery).to receive(:deliver_now)
|
||||
|
||||
worker.perform(org.id, user.id, update_and_notify_user)
|
||||
|
||||
expect(mailer_class).to have_received(:with).with(org_name: org.name, name: user.name, email: user.email)
|
||||
expect(mailer).to have_received(:organization_deleted_email)
|
||||
expect(message_delivery).to have_received(:deliver_now)
|
||||
end
|
||||
end
|
||||
# rubocop:enable RSpec/NestedGroups
|
||||
end
|
||||
|
||||
context "when an org or a user is not found" do
|
||||
it "doesn't fail" do
|
||||
worker.perform(-1, -1)
|
||||
worker.perform(-1, -1, update_and_notify_user)
|
||||
end
|
||||
|
||||
it "doesn't call org delete when a user was not found" do
|
||||
allow(delete).to receive(:call)
|
||||
worker.perform(org.id, -1)
|
||||
worker.perform(org.id, -1, update_and_notify_user)
|
||||
expect(delete).not_to have_received(:call)
|
||||
end
|
||||
|
||||
it "doesn't call org delete when an org was not found" do
|
||||
allow(delete).to receive(:call)
|
||||
worker.perform(-1, org.id)
|
||||
worker.perform(-1, org.id, update_and_notify_user)
|
||||
expect(delete).not_to have_received(:call)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue