Allow ModRole & Admin to Suspend & Unsuspend User (#17946)
* gotta commit at some point * out of gas * IT'S WORKING NOW! 💥 * Cypress tests * fix Travis failures * incorporate PR review suggestions * fix one failing Cypress test * update API call * add Unsuspend flow; fix Travis failures * add tests for unsuspend flow * remove unnecessary atrribute * add article_policy spec * fix failing Travis * nudge Travis * fix apparent typo o o 🤷 * refactor a bunch of duplication * nudge Travis * fix Cypress failure related to authorizing moderator for user_status action * rename modal partial * remove unnecessary button-exists check * address PR review suggestions * hide suspend user btn after action * toggle btn based on author suspension status * add data-testids * use data-testids * controller refactor
This commit is contained in:
parent
64d970abd2
commit
750d379807
22 changed files with 588 additions and 91 deletions
|
|
@ -29,6 +29,17 @@ module Admin
|
|||
Audit::Logger.log(:moderator, current_user, params.dup)
|
||||
end
|
||||
|
||||
# Having this method here (which also exists in admin/application_controller)
|
||||
# allows us to authorize the moderator role specifically for the user_status
|
||||
# action, while preserving the implementation for all other admin actions
|
||||
def authorize_admin
|
||||
if action_name == "user_status"
|
||||
authorize(User, :toggle_suspension_status?)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
@users = Admin::UsersQuery.call(
|
||||
relation: User.registered,
|
||||
|
|
@ -104,18 +115,33 @@ module Admin
|
|||
begin
|
||||
Moderator::ManageActivityAndRoles.handle_user_roles(admin: current_user, user: @user, user_params: user_params)
|
||||
flash[:success] = I18n.t("admin.users_controller.updated")
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_back_or_to admin_users_path
|
||||
end
|
||||
format.json do
|
||||
render json: {
|
||||
success: true,
|
||||
message: I18n.t("admin.users_controller.updated_json", username: @user.username)
|
||||
}, status: :ok
|
||||
end
|
||||
end
|
||||
rescue StandardError => e
|
||||
flash[:danger] = e.message
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_back_or_to admin_users_path
|
||||
end
|
||||
format.json do
|
||||
render json: {
|
||||
success: false,
|
||||
message: @user.errors_as_sentence
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Credits::Manage.call(@user, credit_params)
|
||||
add_note if user_params[:new_note]
|
||||
|
||||
if request.referer&.include?(admin_user_path(params[:id]))
|
||||
redirect_to admin_user_path(params[:id])
|
||||
else
|
||||
redirect_to admin_users_path
|
||||
end
|
||||
end
|
||||
|
||||
def export_data
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { toggleFlagUserModal } from '../packs/flagUserModal';
|
||||
import { toggleModal } from '../packs/toggleUserSuspensionModal';
|
||||
import { toggleUnpublishPostModal } from '../packs/unpublishPostModal';
|
||||
import { request } from '@utilities/http';
|
||||
|
||||
|
|
@ -395,6 +396,14 @@ export function addBottomActionsListeners() {
|
|||
document
|
||||
.getElementById('open-flag-user-modal')
|
||||
.addEventListener('click', toggleFlagUserModal);
|
||||
|
||||
document
|
||||
.getElementById('suspend-user-btn')
|
||||
?.addEventListener('click', toggleModal);
|
||||
|
||||
document
|
||||
.getElementById('unsuspend-user-btn')
|
||||
?.addEventListener('click', toggleModal);
|
||||
}
|
||||
|
||||
export function initializeActionsPanel() {
|
||||
|
|
|
|||
|
|
@ -31,5 +31,5 @@ export const BasicEditor = ({ openModal }) => (
|
|||
);
|
||||
|
||||
BasicEditor.propTypes = {
|
||||
toggleModal: PropTypes.func.isRequired,
|
||||
openModal: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export const Modal = ({
|
|||
backdropDismissible = false,
|
||||
onClose = () => {},
|
||||
focusTrapSelector = '.crayons-modal__box',
|
||||
document = window.document,
|
||||
}) => {
|
||||
const classes = classNames('crayons-modal', {
|
||||
[`crayons-modal--${size}`]: size && size !== 'medium',
|
||||
|
|
@ -36,6 +37,7 @@ export const Modal = ({
|
|||
onDeactivate={onClose}
|
||||
clickOutsideDeactivates={backdropDismissible}
|
||||
selector={focusTrapSelector}
|
||||
document={document}
|
||||
>
|
||||
<div data-testid="modal-container" className={classes}>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const handleConfirmDelete = (username, formAction) => {
|
|||
// Update cancel button to close the modal
|
||||
document
|
||||
.querySelector(`#${WINDOW_MODAL_ID} .js-gdpr-cancel-deleted`)
|
||||
.addEventListener('click', closeWindowModal);
|
||||
.addEventListener('click', () => closeWindowModal());
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ window.Forem = {
|
|||
);
|
||||
},
|
||||
showModal: showWindowModal,
|
||||
closeModal: closeWindowModal,
|
||||
closeModal: () => closeWindowModal(),
|
||||
Runtime,
|
||||
};
|
||||
|
||||
|
|
|
|||
144
app/javascript/packs/toggleUserSuspensionModal.js
Normal file
144
app/javascript/packs/toggleUserSuspensionModal.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import {
|
||||
closeWindowModal,
|
||||
showWindowModal,
|
||||
WINDOW_MODAL_ID,
|
||||
} from '@utilities/showModal';
|
||||
import { request } from '@utilities/http';
|
||||
|
||||
const suspendOrUnsuspendUser = async ({
|
||||
event,
|
||||
btnAction,
|
||||
userId,
|
||||
username,
|
||||
suspendOrUnsuspendReason,
|
||||
}) => {
|
||||
event.preventDefault();
|
||||
closeModal();
|
||||
|
||||
try {
|
||||
const response = await request(
|
||||
`/admin/member_manager/users/${userId}/user_status`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
id: userId,
|
||||
user: {
|
||||
note_for_current_role: suspendOrUnsuspendReason,
|
||||
user_status: btnAction == 'suspend' ? 'Suspended' : 'Good standing',
|
||||
},
|
||||
}),
|
||||
credentials: 'same-origin',
|
||||
},
|
||||
);
|
||||
|
||||
const outcome = await response.json();
|
||||
|
||||
if (outcome.success) {
|
||||
top.addSnackbarItem({
|
||||
message: outcome.message,
|
||||
addCloseButton: true,
|
||||
});
|
||||
|
||||
updateBtnFlow(btnAction, username);
|
||||
} else {
|
||||
top.addSnackbarItem({
|
||||
message: 'Error: something went wrong.',
|
||||
addCloseButton: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
top.addSnackbarItem({
|
||||
message: `Error: ${error}`,
|
||||
addCloseButton: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function capitalizeFirst(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
function updateBtnFlow(btnAction, username) {
|
||||
const btn = window.parent.document
|
||||
.getElementById('mod-container')
|
||||
.contentDocument.querySelector(`#${btnAction}-user-btn`);
|
||||
const oppositeAction = btnAction == 'suspend' ? 'unsuspend' : 'suspend';
|
||||
|
||||
btn.textContent = `${capitalizeFirst(oppositeAction)} ${username}`;
|
||||
btn.dataset.modalTitle = `${capitalizeFirst(oppositeAction)} ${username}`;
|
||||
btn.id = `${oppositeAction}-user-btn`;
|
||||
btn.dataset.modalContentSelector = `#${oppositeAction}-modal-content`;
|
||||
|
||||
oppositeAction == 'suspend'
|
||||
? btn.classList.add('c-btn--destructive')
|
||||
: btn.classList.remove('c-btn--destructive');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
closeWindowModal(window.parent.document);
|
||||
}
|
||||
|
||||
// let modalContents;
|
||||
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 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 =
|
||||
window.parent.document.querySelector(modalContentSelector);
|
||||
const modalContent = modalContentElement.innerHTML;
|
||||
|
||||
modalContentElement.remove();
|
||||
modalContents.set(modalContentSelector, modalContent);
|
||||
}
|
||||
|
||||
return modalContents.get(modalContentSelector);
|
||||
};
|
||||
|
||||
function checkReason(event) {
|
||||
const { btnAction, reasonSelector, userId, username } = event.target.dataset;
|
||||
const modal = window.parent.document.getElementById(WINDOW_MODAL_ID);
|
||||
const actionReason = modal.querySelector(reasonSelector).value;
|
||||
|
||||
if (!actionReason) {
|
||||
modal.querySelector(`.${btnAction}-reason-error`).innerText =
|
||||
'You must give a reason for this action.';
|
||||
} else {
|
||||
suspendOrUnsuspendUser({
|
||||
event,
|
||||
btnAction,
|
||||
userId,
|
||||
username,
|
||||
actionReason,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function activateModalSubmitBtn() {
|
||||
const suspendBtn = window.parent.document.getElementById(
|
||||
'submit-user-suspend-btn',
|
||||
);
|
||||
const unsuspendBtn = window.parent.document.getElementById(
|
||||
'submit-user-unsuspend-btn',
|
||||
);
|
||||
|
||||
suspendBtn?.addEventListener('click', checkReason);
|
||||
unsuspendBtn?.addEventListener('click', checkReason);
|
||||
}
|
||||
|
||||
export function toggleModal(event) {
|
||||
event.preventDefault;
|
||||
const { modalTitle, modalSize, modalContentSelector } = event.target.dataset;
|
||||
showWindowModal({
|
||||
document: window.parent.document,
|
||||
modalContent: getModalContents(modalContentSelector),
|
||||
title: modalTitle,
|
||||
size: modalSize,
|
||||
onOpen: activateModalSubmitBtn,
|
||||
});
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ export const FocusTrap = ({
|
|||
children,
|
||||
onDeactivate,
|
||||
clickOutsideDeactivates = false,
|
||||
document = window.document,
|
||||
}) => {
|
||||
const focusTrap = useRef(null);
|
||||
const deactivate = useCallback(() => onDeactivate(), [onDeactivate]);
|
||||
|
|
@ -51,6 +52,7 @@ export const FocusTrap = ({
|
|||
escapeDeactivates: false,
|
||||
clickOutsideDeactivates,
|
||||
onDeactivate: deactivate,
|
||||
document,
|
||||
});
|
||||
|
||||
focusTrap.current.activate();
|
||||
|
|
@ -62,7 +64,7 @@ export const FocusTrap = ({
|
|||
focusTrap.current.deactivate();
|
||||
routeChangeObserver.disconnect();
|
||||
};
|
||||
}, [clickOutsideDeactivates, selector, deactivate]);
|
||||
}, [clickOutsideDeactivates, selector, deactivate, document]);
|
||||
|
||||
const shortcuts = {
|
||||
escape: onDeactivate,
|
||||
|
|
|
|||
|
|
@ -32,11 +32,13 @@ const getModalImports = () => {
|
|||
* @param {HTMLElement} args.modalContent The HTML to display inside of the modal
|
||||
* @param {string} args.contentSelector The CSS query to locate the HTML to be presented in the modal, as an alternative to passing the actual HTML (e.g. '#my-modal-content')
|
||||
* @param {Function} args.onOpen A callback function to run when the modal opens. This can be useful, for example, to attach any event listeners to items inside the modal.
|
||||
* @param {Object} args.document Allows us to specify which "document" is relevant; e.g. use within iframes.
|
||||
*/
|
||||
export const showWindowModal = async ({
|
||||
modalContent,
|
||||
contentSelector,
|
||||
onOpen,
|
||||
document = window.document,
|
||||
...modalProps
|
||||
}) => {
|
||||
const [{ Modal }, { render, h }] = await getModalImports();
|
||||
|
|
@ -57,6 +59,7 @@ export const showWindowModal = async ({
|
|||
render(null, currentModalContainer);
|
||||
}}
|
||||
focusTrapSelector={`#${WINDOW_MODAL_ID}`}
|
||||
document={document}
|
||||
{...modalProps}
|
||||
>
|
||||
<div
|
||||
|
|
@ -77,7 +80,7 @@ export const showWindowModal = async ({
|
|||
/**
|
||||
* This helper function closes any currently open window modal. This can be useful, for example, if your modal contains a "cancel" button.
|
||||
*/
|
||||
export const closeWindowModal = async () => {
|
||||
export const closeWindowModal = async (document = window.document) => {
|
||||
const currentModalContainer = document.getElementById(WINDOW_MODAL_ID);
|
||||
if (currentModalContainer) {
|
||||
const { render } = await getPreactImport();
|
||||
|
|
|
|||
|
|
@ -149,9 +149,10 @@ class ArticlePolicy < ApplicationPolicy
|
|||
user_any_admin? || user_moderator?
|
||||
end
|
||||
|
||||
# this method performs the same checks that determine
|
||||
# if the record can be featured or if user can adjust
|
||||
# any tag
|
||||
# this method performs the same checks that determine:
|
||||
# if the record can be featured
|
||||
# if user can adjust any tag
|
||||
# if user can perform moderator actions
|
||||
def revoke_publication?
|
||||
require_user!
|
||||
return false unless @record.published?
|
||||
|
|
@ -211,6 +212,8 @@ class ArticlePolicy < ApplicationPolicy
|
|||
|
||||
alias can_adjust_any_tag? revoke_publication?
|
||||
|
||||
alias can_perform_moderator_actions? revoke_publication?
|
||||
|
||||
# Due to the associated controller method "admin_unpublish", we
|
||||
# alias "admin_ubpublish" to the "revoke_publication" method.
|
||||
alias admin_unpublish? revoke_publication?
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ class UserPolicy < ApplicationPolicy
|
|||
current_user?
|
||||
end
|
||||
|
||||
alias remove_identity? edit?
|
||||
alias update_password? edit?
|
||||
|
||||
# The analytics? policy method is also on the OrganizationPolicy. This exists specifically to allow for
|
||||
# "duck-typing" on the tests.
|
||||
alias analytics? edit?
|
||||
|
|
@ -82,8 +85,6 @@ class UserPolicy < ApplicationPolicy
|
|||
OrganizationMembership.exists?(user_id: user.id, organization_id: record.id)
|
||||
end
|
||||
|
||||
alias remove_identity? edit?
|
||||
|
||||
def dashboard_show?
|
||||
current_user? || user_super_admin? || user_any_admin?
|
||||
end
|
||||
|
|
@ -92,12 +93,12 @@ class UserPolicy < ApplicationPolicy
|
|||
user_any_admin? || user_moderator?
|
||||
end
|
||||
|
||||
alias toggle_suspension_status? elevated_user?
|
||||
|
||||
def moderation_routes?
|
||||
(user.has_trusted_role? || elevated_user?) && !user.suspended?
|
||||
end
|
||||
|
||||
alias update_password? edit?
|
||||
|
||||
def permitted_attributes
|
||||
PERMITTED_ATTRIBUTES
|
||||
end
|
||||
|
|
|
|||
|
|
@ -220,6 +220,8 @@
|
|||
<div class="mod-actions-menu print-hidden"></div>
|
||||
<div data-testid="flag-user-modal-container" class="flag-user-modal-container hidden"></div>
|
||||
<div data-testid="unpublish-post-modal-container" class="unpublish-post-modal-container hidden"></div>
|
||||
<%= render "moderations/suspend_user_modal" %>
|
||||
<%= render "moderations/unsuspend_user_modal" %>
|
||||
|
||||
<div class="fullscreen-code js-fullscreen-code"></div>
|
||||
<%= javascript_packs_with_chunks_tag "followButtons", defer: true %>
|
||||
|
|
|
|||
41
app/views/moderations/_suspend_user_modal.html.erb
Normal file
41
app/views/moderations/_suspend_user_modal.html.erb
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<% user_id = @article.user.id %>
|
||||
<% author_username = @article.username %>
|
||||
<div id="suspend-modal-content" class="hidden">
|
||||
<p>
|
||||
<%= t("views.moderations.actions.suspend.suspend_modal_text", username: author_username) %>
|
||||
</p>
|
||||
|
||||
<div class="crayons-field mt-4">
|
||||
<label
|
||||
for="suspend-reason-userid-<%= user_id %>"
|
||||
class="crayons-field__label">
|
||||
Note:
|
||||
</label>
|
||||
<textarea
|
||||
id="suspend-reason-userid-<%= user_id %>"
|
||||
rows="4"
|
||||
cols="70"
|
||||
placeholder="Reason for suspension"
|
||||
class="crayons-textfield"
|
||||
aria-describedby="suspension-reason-error"
|
||||
required></textarea>
|
||||
<p
|
||||
id="suspension-reason-error"
|
||||
data-testid="suspension-reason-error"
|
||||
class="suspend-reason-error crayons-field__description"
|
||||
style="color: red;"
|
||||
aria-live="assertive">
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button
|
||||
id="submit-user-suspend-btn"
|
||||
class="c-btn c-btn--primary c-btn--destructive"
|
||||
data-btn-action="suspend"
|
||||
data-reason-selector="#suspend-reason-userid-<%= user_id %>"
|
||||
data-user-id="<%= user_id %>"
|
||||
data-username="<%= author_username %>">
|
||||
<%= t("views.moderations.actions.suspend.suspend_modal_button", username: author_username) %>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
41
app/views/moderations/_unsuspend_user_modal.html.erb
Normal file
41
app/views/moderations/_unsuspend_user_modal.html.erb
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<% user_id = @article.user.id %>
|
||||
<% author_username = @article.username %>
|
||||
<div id="unsuspend-modal-content" class="hidden">
|
||||
<p>
|
||||
<%= t("views.moderations.actions.suspend.unsuspend_modal_text", username: author_username) %>
|
||||
</p>
|
||||
|
||||
<div class="crayons-field mt-4">
|
||||
<label
|
||||
for="unsuspend-reason-userid-<%= user_id %>"
|
||||
class="crayons-field__label">
|
||||
Note:
|
||||
</label>
|
||||
<textarea
|
||||
id="unsuspend-reason-userid-<%= user_id %>"
|
||||
rows="4"
|
||||
cols="70"
|
||||
placeholder="Reason for unsuspension"
|
||||
class="crayons-textfield"
|
||||
aria-describedby="unsuspension-reason-error"
|
||||
required></textarea>
|
||||
<p
|
||||
id="unsuspension-reason-error"
|
||||
data-testid="unsuspension-reason-error"
|
||||
class="unsuspend-reason-error crayons-field__description"
|
||||
style="color: red;"
|
||||
aria-live="assertive">
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button
|
||||
id="submit-user-unsuspend-btn"
|
||||
class="c-btn c-btn--primary"
|
||||
data-btn-action="unsuspend"
|
||||
data-reason-selector="#unsuspend-reason-userid-<%= user_id %>"
|
||||
data-user-id="<%= user_id %>"
|
||||
data-username="<%= author_username %>">
|
||||
<%= t("views.moderations.actions.suspend.unsuspend_modal_button", username: author_username) %>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -8,6 +8,9 @@
|
|||
|
||||
<%= javascript_packs_with_chunks_tag "actionsPanel", defer: true %>
|
||||
|
||||
<% user_suspended = @moderatable.user.suspended? %>
|
||||
<% user_username = @moderatable.username %>
|
||||
|
||||
<div class="container mod-container">
|
||||
<header class="top-header">
|
||||
<h1><%= t("views.moderations.actions.heading") %></h1>
|
||||
|
|
@ -61,7 +64,7 @@
|
|||
id="feature-article-btn"
|
||||
data-article-featured="<%= @moderatable.featured %>"
|
||||
data-article-id="<%= @moderatable.id %>"
|
||||
data-article-author="<%= @moderatable.username %>"
|
||||
data-article-author="<%= user_username %>"
|
||||
data-article-slug="<%= @moderatable.slug %>">
|
||||
<%= @moderatable.featured ? t("views.moderations.actions.unfeature") : t("views.moderations.actions.feature") %>
|
||||
</button>
|
||||
|
|
@ -183,22 +186,32 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<%# Hiding this Admin Actions panel until Flag User, Unpublish All Posts and Suspend User are moved into it %>
|
||||
<% if current_user.any_admin? %>
|
||||
<button aria-haspopup="true" aria-expanded="false" aria-controls="admin-action-options" class="other-things-btn admin-action hidden" type="button" data-other-things-type="admin-action" aria-label="<%= t("views.moderations.actions.admin.aria_label") %>">
|
||||
<% if policy(@moderatable).can_perform_moderator_actions? %>
|
||||
<button aria-haspopup="true" aria-expanded="false" aria-controls="admin-action-options" class="other-things-btn admin-action" type="button" data-other-things-type="admin-action" aria-label="<%= t("views.moderations.actions.admin.aria_label") %>">
|
||||
<div class="label-wrapper">
|
||||
<div class="icon circle centered-icon">
|
||||
<%= inline_svg_tag("admin.svg", aria_hidden: true, title: t("views.moderations.actions.admin.icon")) %>
|
||||
</div>
|
||||
<header>
|
||||
<h2><%= t("views.moderations.actions.admin.heading") %></h2>
|
||||
<h3><%= t("views.moderations.actions.admin.subtitle") %></h3>
|
||||
<h3><%= t("views.moderations.actions.admin.subtitle", user: user_username) %></h3>
|
||||
</header>
|
||||
</div>
|
||||
<div class="toggle-chevron-container">
|
||||
<%= inline_svg_tag("arrow-down-fill.svg", aria: true, title: t("views.moderations.actions.toggle")) %>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div id="admin-action-options" class="admin-action-options dropdown-options mt-4 hidden">
|
||||
<button
|
||||
type="button"
|
||||
id="<%= user_suspended ? "unsuspend" : "suspend" %>-user-btn"
|
||||
class="js-suspend-unsuspend-flow-btn c-btn <%= user_suspended ? "" : "c-btn--destructive" %> align-center fs-s"
|
||||
data-modal-title="<%= user_suspended ? "Unsuspend" : "Suspend" %> <%= user_username %>"
|
||||
data-modal-size="small" data-modal-content-selector="#<%= user_suspended ? "unsuspend" : "suspend" %>-modal-content" data-username="<%= user_username %>">
|
||||
<%= t("views.moderations.actions.suspend.#{user_suspended ? 'unsuspend' : 'suspend'}_user", username: user_username) %>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ en:
|
|||
updated: Broadcast has been updated!
|
||||
wrong: Something went wrong with deleting the broadcast.
|
||||
consumer_apps_controller:
|
||||
created: "%{app} has been created!"
|
||||
deleted: "%{app} has been deleted!"
|
||||
updated: "%{app} has been updated!"
|
||||
created: '%{app} has been created!'
|
||||
deleted: '%{app} has been deleted!'
|
||||
updated: '%{app} has been updated!'
|
||||
wrong: Something went wrong with deleting %{app}.
|
||||
display_ads_controller:
|
||||
created: Display Ad has been created!
|
||||
|
|
@ -30,11 +30,11 @@ en:
|
|||
updated: Display Ad has been updated!
|
||||
wrong: Something went wrong with deleting the Display Ad.
|
||||
gdpr_delete_requests_controller:
|
||||
deleted: Successfully marked as deleted
|
||||
deleted: Successfully marked as deleted
|
||||
extensions_controller:
|
||||
update_success: Extensions have been updated.
|
||||
html_variants_controller:
|
||||
fork: "%{name} FORK-%{rand}"
|
||||
fork: '%{name} FORK-%{rand}'
|
||||
created: HTML Variant has been created!
|
||||
deleted: HTML Variant has been deleted!
|
||||
updated: HTML Variant has been updated!
|
||||
|
|
@ -53,7 +53,7 @@ en:
|
|||
destroyed: "'%{title}' was destroyed successfully"
|
||||
no_credit: Not enough available credits
|
||||
mods_controller:
|
||||
trusted: "%{username} now has a Trusted role!"
|
||||
trusted: '%{username} now has a Trusted role!'
|
||||
navigation_links_controller:
|
||||
deleted: Navigation Link %{link} deleted
|
||||
created: 'Successfully created navigation link: %{link}'
|
||||
|
|
@ -99,7 +99,7 @@ en:
|
|||
secrets_controller:
|
||||
not_in_vault: Not In Vault
|
||||
updated: Secret %{key} was successfully updated in Vault.
|
||||
value: "%{first8}******"
|
||||
value: '%{first8}******'
|
||||
settings_controller:
|
||||
confirmation: My username is @%{username} and this action is 100% safe and appropriate.
|
||||
spaces_controller:
|
||||
|
|
@ -111,24 +111,24 @@ en:
|
|||
destroyed: Sponsorship was successfully destroyed
|
||||
tags:
|
||||
moderators_controller:
|
||||
not_found: "User ID #%{user_id} was not found"
|
||||
not_found: 'User ID #%{user_id} was not found'
|
||||
not_found_or: 'User ID #%{user_id} was not found, or their account has errors: %{errors}'
|
||||
removed: "@%{username} - ID #%{user_id} was removed as a tag moderator."
|
||||
added: "%{username} was added as a tag moderator!"
|
||||
removed: '@%{username} - ID #%{user_id} was removed as a tag moderator.'
|
||||
added: '%{username} was added as a tag moderator!'
|
||||
tags_controller:
|
||||
created: "%{tag_name} has been created!"
|
||||
updated: "%{tag_name} tag successfully updated!"
|
||||
created: '%{tag_name} has been created!'
|
||||
updated: '%{tag_name} tag successfully updated!'
|
||||
update_fail: 'The tag update failed: %{errors}'
|
||||
tools_controller:
|
||||
article_busted: 'Article #%{article} was successfully busted'
|
||||
user_busted: 'User #%{user} was successfully busted'
|
||||
link_busted: "%{link} was successfully busted"
|
||||
link_busted: '%{link} was successfully busted'
|
||||
users_controller:
|
||||
exported: Data exported to the %{receiver}. The job will complete momentarily.
|
||||
email_fail: Email failed to send!
|
||||
email_sent: Email sent!
|
||||
verify_sent: Verification email sent!
|
||||
full_delete_html: "@%{user} (email: %{email}, user_id: %{id}) has been fully deleted. If this is a GDPR delete, delete them from Mailchimp & Google Analytics and confirm on %{the_page}."
|
||||
full_delete_html: '@%{user} (email: %{email}, user_id: %{id}) has been fully deleted. If this is a GDPR delete, delete them from Mailchimp & Google Analytics and confirm on %{the_page}.'
|
||||
no_email: no email
|
||||
parameter_missing: Both subject and body are required!
|
||||
role_removed: 'Role: %{role} has been successfully removed from the user!'
|
||||
|
|
@ -138,6 +138,7 @@ en:
|
|||
unlocked: Unlocked User account!
|
||||
unpublished: Posts are being unpublished in the background. The job will complete soon.
|
||||
updated: User has been updated
|
||||
updated_json: Success! %{username} has been updated.
|
||||
credits_added: Credits have been added!
|
||||
credits_removed: Credits have been removed.
|
||||
welcome_controller:
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ fr:
|
|||
updated: Broadcast has been updated!
|
||||
wrong: Something went wrong with deleting the broadcast.
|
||||
consumer_apps_controller:
|
||||
created: "%{app} has been created!"
|
||||
deleted: "%{app} has been deleted!"
|
||||
updated: "%{app} has been updated!"
|
||||
created: '%{app} has been created!'
|
||||
deleted: '%{app} has been deleted!'
|
||||
updated: '%{app} has been updated!'
|
||||
wrong: Something went wrong with deleting %{app}.
|
||||
display_ads_controller:
|
||||
created: Display Ad has been created!
|
||||
|
|
@ -32,9 +32,9 @@ fr:
|
|||
extensions_controller:
|
||||
update_success: Les extensions ont été mises à jour.
|
||||
gdpr_delete_requests_controller:
|
||||
deleted: Successfully marked as deleted
|
||||
deleted: Successfully marked as deleted
|
||||
html_variants_controller:
|
||||
fork: "%{name} FORK-%{rand}"
|
||||
fork: '%{name} FORK-%{rand}'
|
||||
created: HTML Variant has been created!
|
||||
deleted: HTML Variant has been deleted!
|
||||
updated: HTML Variant has been updated!
|
||||
|
|
@ -53,7 +53,7 @@ fr:
|
|||
destroyed: "'%{title}' was destroyed successfully"
|
||||
no_credit: Not enough available credits
|
||||
mods_controller:
|
||||
trusted: "%{username} now has a Trusted role!"
|
||||
trusted: '%{username} now has a Trusted role!'
|
||||
navigation_links_controller:
|
||||
deleted: Navigation Link %{link} deleted
|
||||
created: 'Successfully created navigation link: %{link}'
|
||||
|
|
@ -99,7 +99,7 @@ fr:
|
|||
secrets_controller:
|
||||
not_in_vault: Not In Vault
|
||||
updated: Secret %{key} was successfully updated in Vault.
|
||||
value: "%{first8}******"
|
||||
value: '%{first8}******'
|
||||
settings_controller:
|
||||
confirmation: My username is @%{username} and this action is 100% safe and appropriate.
|
||||
spaces_controller:
|
||||
|
|
@ -111,24 +111,24 @@ fr:
|
|||
destroyed: Sponsorship was successfully destroyed
|
||||
tags:
|
||||
moderators_controller:
|
||||
not_found: "User ID #%{user_id} was not found"
|
||||
not_found: 'User ID #%{user_id} was not found'
|
||||
not_found_or: 'User ID #%{user_id} was not found, or their account has errors: %{errors}'
|
||||
removed: "@%{username} - ID #%{user_id} was removed as a tag moderator."
|
||||
added: "%{username} was added as a tag moderator!"
|
||||
removed: '@%{username} - ID #%{user_id} was removed as a tag moderator.'
|
||||
added: '%{username} was added as a tag moderator!'
|
||||
tags_controller:
|
||||
created: "%{tag_name} has been created!"
|
||||
updated: "%{tag_name} tag successfully updated!"
|
||||
created: '%{tag_name} has been created!'
|
||||
updated: '%{tag_name} tag successfully updated!'
|
||||
update_fail: 'The tag update failed: %{errors}'
|
||||
tools_controller:
|
||||
article_busted: 'Article #%{article} was successfully busted'
|
||||
user_busted: 'User #%{user} was successfully busted'
|
||||
link_busted: "%{link} was successfully busted"
|
||||
link_busted: '%{link} was successfully busted'
|
||||
users_controller:
|
||||
exported: Data exported to the %{receiver}. The job will complete momentarily.
|
||||
email_fail: Email failed to send!
|
||||
email_sent: Email sent!
|
||||
verify_sent: Verification email sent!
|
||||
full_delete_html: "@%{user} (email: %{email}, user_id: %{id}) has been fully deleted. If this is a GDPR delete, delete them from Mailchimp & Google Analytics and confirm on %{the_page}."
|
||||
full_delete_html: '@%{user} (email: %{email}, user_id: %{id}) has been fully deleted. If this is a GDPR delete, delete them from Mailchimp & Google Analytics and confirm on %{the_page}.'
|
||||
no_email: no email
|
||||
parameter_missing: Both subject and body are required!
|
||||
role_removed: 'Role: %{role} has been successfully removed from the user!'
|
||||
|
|
@ -138,6 +138,7 @@ fr:
|
|||
unlocked: Unlocked User account!
|
||||
unpublished: Posts are being unpublished in the background. The job will complete soon.
|
||||
updated: User has been updated
|
||||
updated_json: Success! %{username} has been updated.
|
||||
credits_added: Credits have been added!
|
||||
credits_removed: Credits have been removed.
|
||||
welcome_controller:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ en:
|
|||
actions:
|
||||
meta:
|
||||
title: Moderating %{title}
|
||||
title2: "[Moderate] %{title}"
|
||||
title2: '[Moderate] %{title}'
|
||||
heading: Moderate Post
|
||||
subtitle: Rate the quality of this post
|
||||
abusive:
|
||||
|
|
@ -38,7 +38,7 @@ en:
|
|||
resource_admin: ResourceAdmin:Article
|
||||
errors:
|
||||
one: '1 error prohibited this block from being saved:'
|
||||
other: "%{count} errors prohibited this block from being saved:"
|
||||
other: '%{count} errors prohibited this block from being saved:'
|
||||
experience:
|
||||
heading: Set experience level
|
||||
heading2: Experience Level of Post
|
||||
|
|
@ -46,7 +46,7 @@ en:
|
|||
aria_label: Open experience level section
|
||||
icon: Book
|
||||
desc_html: Who <em>might</em> find this post most valuable, based on overall experience level?
|
||||
from: "%{lvl} - "
|
||||
from: '%{lvl} - '
|
||||
level:
|
||||
Advanced: Advanced
|
||||
Beginner: Beginner
|
||||
|
|
@ -54,9 +54,9 @@ en:
|
|||
Mid-level: Mid-level
|
||||
Novice: Novice
|
||||
admin:
|
||||
heading: Admin actions
|
||||
heading2: Higher permission post options
|
||||
subtitle: Higher permission post options
|
||||
heading: Moderating actions
|
||||
heading2: Moderating actions for %{user}
|
||||
subtitle: Moderating actions for %{user}
|
||||
aria_label: Open admin actions
|
||||
icon: Crown
|
||||
flag: Flag user
|
||||
|
|
@ -74,20 +74,27 @@ en:
|
|||
Use <b>Flag to Admins</b> for code of conduct violations (harassment, being a jerk, spam, etc.).
|
||||
other: Other things you can do
|
||||
add_reaction: Add a reaction
|
||||
suspend:
|
||||
suspend_user: Suspend %{username}
|
||||
suspend_modal_text: Once suspended, %{username} will not be able to comment or publish posts until their suspension is removed.
|
||||
suspend_modal_button: Submit & Suspend
|
||||
unsuspend_user: Unsuspend %{username}
|
||||
unsuspend_modal_text: Once unsuspended, %{username} will be able to comment and publish posts again.
|
||||
unsuspend_modal_button: Submit & Unsuspend
|
||||
suspicious: Suspicious
|
||||
tag:
|
||||
subtitle: Tag Adjustments
|
||||
add: Add
|
||||
added_html: "<b>Currently added tag:</b> %{tag}"
|
||||
live_html: "<b>Current live tags:</b> %{tags}"
|
||||
added_html: '<b>Currently added tag:</b> %{tag}'
|
||||
live_html: '<b>Current live tags:</b> %{tags}'
|
||||
reason: Reason for adjustment (Be super kind) - Only the reason is needed, the notification will take care of the rest.
|
||||
remove: Remove
|
||||
removed_html: "<b>Currently removed tag:</b> %{tag}"
|
||||
removed_html: '<b>Currently removed tag:</b> %{tag}'
|
||||
select: Select Tag
|
||||
submit: Submit Tag Adjustment
|
||||
tag_name: Tag Name
|
||||
undo:
|
||||
button: "×"
|
||||
button: '×'
|
||||
confirm: Are you sure you want to undo the %{type} of the %{tag} tag?
|
||||
type:
|
||||
addition: addition
|
||||
|
|
@ -102,8 +109,8 @@ en:
|
|||
flag_to_admins: Flag to Admins
|
||||
vote_up: High Quality
|
||||
featured_past_day:
|
||||
one: "%{count} post from the past day is featured."
|
||||
other: "%{count} posts from the past day are featured."
|
||||
one: '%{count} post from the past day is featured.'
|
||||
other: '%{count} posts from the past day are featured.'
|
||||
all: All topics
|
||||
aside:
|
||||
all: All topics
|
||||
|
|
@ -112,7 +119,7 @@ en:
|
|||
feedback:
|
||||
subtitle: Have feedback to improve your Mod experience?
|
||||
description: Please email %{email}!
|
||||
hello: "Hello! 👋"
|
||||
hello: 'Hello! 👋'
|
||||
thanks: Thank you for helping to keep %{community} safe! ❤️
|
||||
inbox: Inbox
|
||||
resources: Resources
|
||||
|
|
@ -122,7 +129,7 @@ en:
|
|||
author: Author
|
||||
date: Date
|
||||
notice:
|
||||
subtitle: "%{community} Mods"
|
||||
subtitle: '%{community} Mods'
|
||||
desc1_html: We periodically award some %{community} members with heightened privileges to help moderate the community.
|
||||
desc2_html: Check out our %{code} and read through our %{trusted} and %{tag}.
|
||||
desc3_html: If you'd like to assist us as a trusted user or tag mod, please email us at %{email} and let us know which role you're interested in and why. If it's tag moderation, please tell us what tags you'd like to moderate for.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ fr:
|
|||
actions:
|
||||
meta:
|
||||
title: Moderating %{title}
|
||||
title2: "[Moderate] %{title}"
|
||||
title2: '[Moderate] %{title}'
|
||||
heading: Moderate Post
|
||||
subtitle: Rate the quality of this post
|
||||
abusive:
|
||||
|
|
@ -38,7 +38,7 @@ fr:
|
|||
resource_admin: ResourceAdmin:Article
|
||||
errors:
|
||||
one: '1 error prohibited this block from being saved:'
|
||||
other: "%{count} errors prohibited this block from being saved:"
|
||||
other: '%{count} errors prohibited this block from being saved:'
|
||||
experience:
|
||||
heading: Set experience level
|
||||
heading2: Experience Level of Post
|
||||
|
|
@ -46,7 +46,7 @@ fr:
|
|||
aria_label: Open experience level section
|
||||
icon: Book
|
||||
desc_html: Who <em>might</em> find this post most valuable, based on overall experience level?
|
||||
from: "%{lvl} - "
|
||||
from: '%{lvl} - '
|
||||
level:
|
||||
Advanced: Advanced
|
||||
Beginner: Beginner
|
||||
|
|
@ -54,9 +54,9 @@ fr:
|
|||
Mid-level: Mid-level
|
||||
Novice: Novice
|
||||
admin:
|
||||
heading: Admin actions
|
||||
heading2: Higher permission post options
|
||||
subtitle: Higher permission post options
|
||||
heading: Moderating actions
|
||||
heading2: Moderating actions to %{user}
|
||||
subtitle: Moderating actions to %{user}
|
||||
aria_label: Open admin actions
|
||||
icon: Crown
|
||||
flag: Flag user
|
||||
|
|
@ -73,20 +73,27 @@ fr:
|
|||
<br />
|
||||
Use <b>Flag to Admins</b> for code of conduct violations (harassment, being a jerk, spam, etc.).
|
||||
other: Other things you can do
|
||||
suspend:
|
||||
suspend_user: Suspend %{username}
|
||||
suspend_modal_text: Once suspended, %{username} will not be able to comment or publish posts until their suspension is removed.
|
||||
suspend_modal_button: Submit & Suspend
|
||||
unsuspend_user: Unsuspend %{username}
|
||||
unsuspend_modal_text: Once unsuspended, %{username} will be able to comment and publish posts again.
|
||||
unsuspend_modal_button: Submit & Unsuspend
|
||||
suspicious: Suspicious
|
||||
tag:
|
||||
subtitle: Tag Adjustments
|
||||
add: Add
|
||||
added_html: "<b>Currently added tag:</b> %{tag}"
|
||||
live_html: "<b>Current live tags:</b> %{tags}"
|
||||
added_html: '<b>Currently added tag:</b> %{tag}'
|
||||
live_html: '<b>Current live tags:</b> %{tags}'
|
||||
reason: Reason for adjustment (Be super kind) - Only the reason is needed, the notification will take care of the rest.
|
||||
remove: Remove
|
||||
removed_html: "<b>Currently removed tag:</b> %{tag}"
|
||||
removed_html: '<b>Currently removed tag:</b> %{tag}'
|
||||
select: Select Tag
|
||||
submit: Submit Tag Adjustment
|
||||
tag_name: Tag Name
|
||||
undo:
|
||||
button: "×"
|
||||
button: '×'
|
||||
confirm: Are you sure you want to undo the %{type} of the %{tag} tag?
|
||||
type:
|
||||
addition: addition
|
||||
|
|
@ -101,8 +108,8 @@ fr:
|
|||
flag_to_admins: Flag to Admins
|
||||
vote_up: High Quality
|
||||
featured_past_day:
|
||||
one: "%{count} post from the past day is featured."
|
||||
other: "%{count} posts from the past day are featured."
|
||||
one: '%{count} post from the past day is featured.'
|
||||
other: '%{count} posts from the past day are featured.'
|
||||
all: All topics
|
||||
aside:
|
||||
all: All topics
|
||||
|
|
@ -111,7 +118,7 @@ fr:
|
|||
feedback:
|
||||
subtitle: Have feedback to improve your Mod experience?
|
||||
description: Please email %{email}!
|
||||
hello: "Hello! 👋"
|
||||
hello: 'Hello! 👋'
|
||||
thanks: Thank you for helping to keep %{community} safe! ❤️
|
||||
inbox: Inbox
|
||||
resources: Resources
|
||||
|
|
@ -121,7 +128,7 @@ fr:
|
|||
author: Author
|
||||
date: Date
|
||||
notice:
|
||||
subtitle: "%{community} Mods"
|
||||
subtitle: '%{community} Mods'
|
||||
desc1_html: We periodically award some %{community} members with heightened privileges to help moderate the community.
|
||||
desc2_html: Check out our %{code} and read through our %{trusted} and %{tag}.
|
||||
desc3_html: If you'd like to assist us as a trusted user or tag mod, please email us at %{email} and let us know which role you're interested in and why. If it's tag moderation, please tell us what tags you'd like to moderate for.
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ describe('Moderation Tools for Posts', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should show Feature Post button on an unfeatured post for an admin user', () => {
|
||||
it('should show Feature Post button on an unfeatured post', () => {
|
||||
cy.get('@adminUser').then((user) => {
|
||||
cy.loginAndVisit(user, '/admin_mcadmin/unfeatured-article-slug').then(
|
||||
() => {
|
||||
|
|
@ -68,7 +68,7 @@ describe('Moderation Tools for Posts', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should show Unfeature Post button on a featured post for an admin user', () => {
|
||||
it('should show Unfeature Post button on a featured post', () => {
|
||||
cy.get('@adminUser').then((user) => {
|
||||
cy.loginAndVisit(user, '/admin_mcadmin/test-article-slug').then(() => {
|
||||
cy.findByRole('button', { name: 'Moderation' }).click();
|
||||
|
|
@ -80,7 +80,7 @@ describe('Moderation Tools for Posts', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should show Unpublish Post button on a published post for an admin user', () => {
|
||||
it('should show Unpublish Post button on a published post', () => {
|
||||
cy.get('@adminUser').then((user) => {
|
||||
cy.loginAndVisit(user, '/admin_mcadmin/test-article-slug').then(() => {
|
||||
cy.findByRole('button', { name: 'Moderation' }).click();
|
||||
|
|
@ -93,12 +93,12 @@ describe('Moderation Tools for Posts', () => {
|
|||
});
|
||||
});
|
||||
|
||||
context('as moderator user', () => {
|
||||
describe('moderator user', () => {
|
||||
beforeEach(() => {
|
||||
cy.fixture('users/moderatorUser.json').as('moderatorUser');
|
||||
});
|
||||
|
||||
it('should load moderation tools on a post for a moderator user', () => {
|
||||
it('should load moderation tools on a post', () => {
|
||||
cy.get('@moderatorUser').then((user) => {
|
||||
cy.loginAndVisit(user, '/admin_mcadmin/test-article-slug').then(() => {
|
||||
cy.findByRole('button', { name: 'Moderation' }).should('exist');
|
||||
|
|
@ -106,7 +106,7 @@ describe('Moderation Tools for Posts', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should not show Feature Post button on a post for a moderator user', () => {
|
||||
it('should not show Feature Post button on a post', () => {
|
||||
cy.get('@moderatorUser').then((user) => {
|
||||
cy.loginAndVisit(user, '/admin_mcadmin/unfeatured-article-slug').then(
|
||||
() => {
|
||||
|
|
@ -124,7 +124,7 @@ describe('Moderation Tools for Posts', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should show Unpublish Post button on a published post for a moderator user', () => {
|
||||
it('should show Unpublish Post button on a published post', () => {
|
||||
cy.get('@moderatorUser').then((user) => {
|
||||
cy.loginAndVisit(user, '/admin_mcadmin/test-article-slug').then(() => {
|
||||
cy.findByRole('button', { name: 'Moderation' }).click();
|
||||
|
|
@ -136,7 +136,7 @@ describe('Moderation Tools for Posts', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should show Adjust tags button on a published post for a moderator user', () => {
|
||||
it('should show Adjust tags button on a published post', () => {
|
||||
cy.get('@moderatorUser').then((user) => {
|
||||
cy.loginAndVisit(user, '/admin_mcadmin/test-article-slug').then(() => {
|
||||
cy.findByRole('button', { name: 'Moderation' }).click();
|
||||
|
|
@ -151,6 +151,157 @@ describe('Moderation Tools for Posts', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('when suspending user', () => {
|
||||
beforeEach(() => {
|
||||
cy.get('@moderatorUser').then((user) => {
|
||||
cy.loginAndVisit(user, '/series_user/series-test-article-slug');
|
||||
cy.findByRole('heading', { level: 1, name: 'Series test article' });
|
||||
cy.findByRole('button', { name: 'Moderation' }).click();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show Suspend User button', () => {
|
||||
cy.getIframeBody('[title="Moderation panel actions"]').within(() => {
|
||||
cy.findByRole('button', { name: 'Open admin actions' })
|
||||
.as('moderatingActionsButton')
|
||||
.pipe(click)
|
||||
.should('have.attr', 'aria-expanded', 'true');
|
||||
|
||||
cy.findByRole('button', {
|
||||
name: 'Suspend series_user',
|
||||
}).should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not suspend the user when no reason given', () => {
|
||||
cy.getIframeBody('[title="Moderation panel actions"]').within(() => {
|
||||
cy.findByRole('button', { name: 'Open admin actions' })
|
||||
.as('moderatingActionsButton')
|
||||
.pipe(click)
|
||||
.should('have.attr', 'aria-expanded', 'true');
|
||||
|
||||
cy.findByRole('button', {
|
||||
name: 'Suspend series_user',
|
||||
}).click();
|
||||
});
|
||||
cy.findByRole('dialog').within(() => {
|
||||
cy.findByRole('button', { name: 'Submit & Suspend' }).click();
|
||||
|
||||
cy.findByTestId('suspension-reason-error')
|
||||
.contains('You must give a reason for this action.')
|
||||
.should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('should suspend the user when suspension reason given', () => {
|
||||
cy.getIframeBody('[title="Moderation panel actions"]').within(() => {
|
||||
cy.findByRole('button', { name: 'Open admin actions' })
|
||||
.as('moderatingActionsButton')
|
||||
.pipe(click)
|
||||
.should('have.attr', 'aria-expanded', 'true');
|
||||
|
||||
cy.findByRole('button', {
|
||||
name: 'Suspend series_user',
|
||||
}).click();
|
||||
});
|
||||
cy.findByRole('dialog').within(() => {
|
||||
cy.findByRole('textbox', { name: 'Note:' }).type(
|
||||
'My suspension reason',
|
||||
);
|
||||
|
||||
cy.findByRole('button', { name: 'Submit & Suspend' }).click();
|
||||
});
|
||||
|
||||
cy.findByTestId('snackbar')
|
||||
.contains('Success! series_user has been updated.')
|
||||
.should('exist');
|
||||
|
||||
cy.getIframeBody('[title="Moderation panel actions"]').within(() => {
|
||||
cy.findByRole('button', { name: 'Open admin actions' })
|
||||
.as('moderatingActionsButton')
|
||||
.pipe(click)
|
||||
.should('have.attr', 'aria-expanded', 'true');
|
||||
|
||||
cy.findByRole('button', {
|
||||
name: 'Suspend series_user',
|
||||
}).should('not.exist');
|
||||
|
||||
cy.findByRole('button', {
|
||||
name: 'Unsuspend series_user',
|
||||
}).should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('when unsuspending user', () => {
|
||||
beforeEach(() => {
|
||||
cy.get('@moderatorUser').then((user) => {
|
||||
cy.loginAndVisit(user, '/suspended_user/suspended-user-article-slug');
|
||||
cy.findByRole('heading', {
|
||||
level: 1,
|
||||
name: 'Suspended user article',
|
||||
});
|
||||
cy.findByRole('button', { name: 'Moderation' }).click();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not unsuspend the user when no reason given', () => {
|
||||
cy.getIframeBody('[title="Moderation panel actions"]').within(() => {
|
||||
cy.findByRole('button', { name: 'Open admin actions' })
|
||||
.as('moderatingActionsButton')
|
||||
.pipe(click)
|
||||
.should('have.attr', 'aria-expanded', 'true');
|
||||
cy.findByRole('button', {
|
||||
name: 'Unsuspend suspended_user',
|
||||
}).click();
|
||||
});
|
||||
cy.findByRole('dialog').within(() => {
|
||||
cy.findByRole('button', { name: 'Submit & Unsuspend' }).click();
|
||||
cy.findByTestId('unsuspension-reason-error')
|
||||
.contains('You must give a reason for this action.')
|
||||
.should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('should unsuspend the user when reason given', () => {
|
||||
cy.getIframeBody('[title="Moderation panel actions"]').within(() => {
|
||||
cy.findByRole('button', { name: 'Open admin actions' })
|
||||
.as('moderatingActionsButton')
|
||||
.pipe(click)
|
||||
.should('have.attr', 'aria-expanded', 'true');
|
||||
cy.findByRole('button', {
|
||||
name: 'Unsuspend suspended_user',
|
||||
}).click();
|
||||
});
|
||||
|
||||
cy.findByRole('dialog').within(() => {
|
||||
cy.findByRole('textbox', { name: 'Note:' }).type(
|
||||
'My unsuspension reason',
|
||||
);
|
||||
cy.findByRole('button', { name: 'Submit & Unsuspend' }).click();
|
||||
});
|
||||
|
||||
cy.findByTestId('snackbar')
|
||||
.contains('Success! suspended_user has been updated.')
|
||||
.should('exist');
|
||||
|
||||
cy.getIframeBody('[title="Moderation panel actions"]').within(() => {
|
||||
cy.findByRole('button', { name: 'Open admin actions' })
|
||||
.as('moderatingActionsButton')
|
||||
.pipe(click)
|
||||
.should('have.attr', 'aria-expanded', 'true');
|
||||
|
||||
cy.findByRole('button', {
|
||||
name: 'Unsuspend suspended_user',
|
||||
}).should('not.exist');
|
||||
|
||||
cy.findByRole('button', {
|
||||
name: 'Suspend suspended_user',
|
||||
}).should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('as trusted user', () => {
|
||||
|
|
@ -158,7 +309,7 @@ describe('Moderation Tools for Posts', () => {
|
|||
cy.fixture('users/trustedUser.json').as('trustedUser');
|
||||
});
|
||||
|
||||
it('should load moderation tools on a post for a trusted user', () => {
|
||||
it('should load moderation tools on a post', () => {
|
||||
cy.get('@trustedUser').then((user) => {
|
||||
cy.loginAndVisit(user, '/admin_mcadmin/test-article-slug').then(() => {
|
||||
cy.findByRole('button', { name: 'Moderation' }).should('exist');
|
||||
|
|
@ -166,7 +317,7 @@ describe('Moderation Tools for Posts', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should not show Feature Post button on a post for a trusted user', () => {
|
||||
it('should not show Feature Post button on a post', () => {
|
||||
cy.get('@trustedUser').then((user) => {
|
||||
cy.loginAndVisit(user, '/admin_mcadmin/unfeatured-article-slug').then(
|
||||
() => {
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ RSpec.describe ArticlePolicy do
|
|||
end
|
||||
|
||||
%i[admin_unpublish? admin_featured_toggle? revoke_publication? toggle_featured_status?
|
||||
can_adjust_any_tag?].each do |method_name|
|
||||
can_adjust_any_tag? can_perform_moderator_actions?].each do |method_name|
|
||||
describe "##{method_name}" do
|
||||
let(:policy_method) { method_name }
|
||||
|
||||
|
|
|
|||
|
|
@ -722,6 +722,48 @@ end
|
|||
|
||||
##############################################################################
|
||||
|
||||
seeder.create_if_doesnt_exist(User, "email", "suspended-user@forem.local") do
|
||||
suspended_user = User.create!(
|
||||
name: "Suspended User",
|
||||
email: "suspended-user@forem.local",
|
||||
username: "suspended_user",
|
||||
profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")),
|
||||
confirmed_at: Time.current,
|
||||
registered_at: Time.current,
|
||||
password: "password",
|
||||
password_confirmation: "password",
|
||||
saw_onboarding: true,
|
||||
checked_code_of_conduct: true,
|
||||
checked_terms_and_conditions: true,
|
||||
)
|
||||
|
||||
suspended_user.add_role(:suspended)
|
||||
end
|
||||
|
||||
##############################################################################
|
||||
|
||||
seeder.create_if_doesnt_exist(Article, "title", "Suspended user article") do
|
||||
markdown = <<~MARKDOWN
|
||||
---
|
||||
title: Suspended user article
|
||||
published: true
|
||||
cover_image: #{Faker::Company.logo}
|
||||
---
|
||||
#{Faker::Hipster.paragraph(sentence_count: 2)}
|
||||
#{Faker::Markdown.random}
|
||||
#{Faker::Hipster.paragraph(sentence_count: 2)}
|
||||
MARKDOWN
|
||||
Article.create(
|
||||
body_markdown: markdown,
|
||||
featured: false,
|
||||
show_comments: true,
|
||||
slug: "suspended-user-article-slug",
|
||||
user_id: User.find_by(email: "suspended-user@forem.local").id,
|
||||
)
|
||||
end
|
||||
|
||||
##############################################################################
|
||||
|
||||
seeder.create_if_doesnt_exist(Article, "title", "Series test article") do
|
||||
markdown = <<~MARKDOWN
|
||||
---
|
||||
|
|
@ -738,6 +780,7 @@ seeder.create_if_doesnt_exist(Article, "title", "Series test article") do
|
|||
body_markdown: markdown,
|
||||
featured: true,
|
||||
show_comments: true,
|
||||
slug: "series-test-article-slug",
|
||||
user_id: User.find_by(email: "series-user@forem.local").id,
|
||||
)
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue