mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-12 00:46:13 -05:00
Add interface for moderation subscriptions and suggestions
This commit is contained in:
96
app/controllers/admin/moderation_subscriptions_controller.rb
Normal file
96
app/controllers/admin/moderation_subscriptions_controller.rb
Normal file
@@ -0,0 +1,96 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Admin::ModerationSubscriptionsController < Admin::BaseController
|
||||
before_action :set_moderation_subscriptions, only: :index
|
||||
before_action :set_moderation_subscription, only: [:show, :edit, :update, :destroy]
|
||||
before_action :set_subscribed_advisories, only: :show
|
||||
|
||||
def index
|
||||
authorize :moderation_subscription, :index?
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @moderation_subscription, :show?
|
||||
end
|
||||
|
||||
def new
|
||||
authorize :moderation_subscription, :create?
|
||||
@moderation_subscription = ModerationSubscription.new
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize @moderation_subscription, :update?
|
||||
end
|
||||
|
||||
def create
|
||||
authorize :moderation_subscription, :create?
|
||||
|
||||
# TODO: require confirmation for dangerous subscriptions
|
||||
|
||||
@moderation_subscription = ModerationSubscription.new(resource_params.merge('type' => 'csv_list'))
|
||||
|
||||
if @moderation_subscription.save
|
||||
log_action :create, @moderation_subscription
|
||||
redirect_to admin_moderation_subscriptions_path
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @moderation_subscription, :update?
|
||||
|
||||
# TODO: require confirmation for dangerous subscriptions
|
||||
|
||||
if @moderation_subscription.update(resource_params)
|
||||
log_action :update, @moderation_subscription
|
||||
redirect_to admin_moderation_subscription_path(@moderation_subscription)
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @moderation_subscription, :destroy?
|
||||
|
||||
ModerationSubscription.transaction do
|
||||
# Re-run moderation list service with empty list to ensure
|
||||
# Retractions are handled properly
|
||||
@moderation_subscription.advisories.delete_all
|
||||
ProcessModerationListsService.new.call
|
||||
|
||||
@moderation_subscription.destroy!
|
||||
log_action :destroy, @moderation_subscription
|
||||
end
|
||||
|
||||
redirect_to admin_moderation_subscriptions_path, notice: I18n.t('admin.moderation_subscriptions.destroyed_msg')
|
||||
end
|
||||
|
||||
def logs
|
||||
authorize :moderation_subscription, :index?
|
||||
|
||||
@action_logs = Admin::ActionLog.latest.where.not(moderation_subscription_id: nil).page(params[:page])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resource_params
|
||||
params.expect(
|
||||
moderation_subscription: [
|
||||
:name, :url, :priority, :list_action, :apply_conditions, :retract_automatically
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def set_moderation_subscriptions
|
||||
@moderation_subscriptions = ModerationSubscription.order(priority: :asc)
|
||||
end
|
||||
|
||||
def set_moderation_subscription
|
||||
@moderation_subscription = ModerationSubscription.find(params[:id])
|
||||
end
|
||||
|
||||
def set_subscribed_advisories
|
||||
@subscribed_advisories = @moderation_subscription.advisories.order(id: :asc).page(params[:page])
|
||||
end
|
||||
end
|
||||
159
app/controllers/admin/moderation_suggestions_controller.rb
Normal file
159
app/controllers/admin/moderation_suggestions_controller.rb
Normal file
@@ -0,0 +1,159 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Admin::ModerationSuggestionsController < Admin::BaseController
|
||||
before_action :set_moderation_suggestion_targets, only: :index
|
||||
before_action :set_moderation_suggestions_by_target, only: :index
|
||||
before_action :set_moderation_advisories_by_target, only: :index
|
||||
before_action :set_current_state_by_target, only: :index
|
||||
before_action :set_moderation_suggestion, only: [:destroy, :apply]
|
||||
|
||||
def index
|
||||
authorize :moderation_suggestion, :index?
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @moderation_suggestion, :dismiss?
|
||||
|
||||
# TODO: log?
|
||||
|
||||
# Actually dismiss all of the suggestions for the same target
|
||||
ModerationSuggestion.where(target_type: @moderation_suggestion.target_type, target_key: @moderation_suggestion.target_key).update_all(state: :dismissed)
|
||||
|
||||
redirect_to admin_moderation_suggestions_path, notice: I18n.t('admin.moderation_suggestions.destroyed_msg', target_key: @moderation_suggestion.target_key)
|
||||
end
|
||||
|
||||
def apply
|
||||
authorize @moderation_suggestion, :apply?
|
||||
|
||||
case [@moderation_suggestion.target_type, @moderation_suggestion.action]
|
||||
when ['domain', 'accept']
|
||||
domain_allow = @moderation_suggestion.to_domain_allow
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
# TODO: log
|
||||
domain_allow.save!
|
||||
@moderation_suggestion.mark_as_applied!
|
||||
end
|
||||
|
||||
redirect_to admin_moderation_suggestions_path, notice: I18n.t('admin.moderation_suggestions.applied_msg', target_key: @moderation_suggestion.target_key)
|
||||
when ['domain', 'reject'], ['domain', 'limit']
|
||||
domain_block = @moderation_suggestion.to_domain_block
|
||||
|
||||
# TODO: factor with `DomainBlocksController#create`?
|
||||
existing_domain_block = DomainBlock.rule_for(domain_block.domain)
|
||||
|
||||
# We can't create a laxer block for a subdomain than we have for a domain
|
||||
if existing_domain_block.present? && existing_domain_block.domain != TagManager.instance.normalize_domain(domain_block.domain) && !domain_block.stricter_than?(existing_domain_block)
|
||||
return redirect_to admin_moderation_suggestions_path,
|
||||
alert: I18n.t('admin.moderation_suggestions.unable_to_apply_msg',
|
||||
target_key: @moderation_suggestion.target_key)
|
||||
end
|
||||
|
||||
update = false
|
||||
|
||||
# Allow transparently upgrading a domain block
|
||||
if existing_domain_block.present? && existing_domain_block.domain == TagManager.instance.normalize_domain(domain_block.domain)
|
||||
# Downgrading requires confirmation
|
||||
return render :confirm_downgrade unless domain_block.stricter_than?(existing_domain_block) || params[:confirm_downgrade]
|
||||
|
||||
# Transparent upgrading is allowed
|
||||
existing_domain_block.assign_attributes(domain_block.attributes.without('id', 'created_at', 'updated_at'))
|
||||
domain_block = existing_domain_block
|
||||
|
||||
update = domain_block.severity_changed?
|
||||
end
|
||||
|
||||
# Require explicit confirmation on block
|
||||
return render :confirm_reject if requires_confirmation?(domain_block)
|
||||
|
||||
domain_block.save!
|
||||
# TODO: is this the way we want to log it?
|
||||
log_action (update ? :update : :create), domain_block, moderation_subscription_id: domain_block.moderation_subscription_id
|
||||
DomainBlockWorker.perform_async(domain_block.id, update)
|
||||
@moderation_suggestion.mark_as_applied!
|
||||
|
||||
redirect_to admin_moderation_suggestions_path, notice: I18n.t('admin.moderation_suggestions.applied_msg', target_key: @moderation_suggestion.target_key)
|
||||
when ['domain', 'retract']
|
||||
ApplicationRecord.transaction do
|
||||
if Rails.configuration.x.mastodon.limited_federation_mode
|
||||
domain_allow = DomainAllow.find_by(domain: @moderation_suggestion.target_key)
|
||||
UnallowDomainService.new.call(domain_allow)
|
||||
# TODO: is this the way we want to log it?
|
||||
log_action :destroy, domain_allow, moderation_subscription_id: domain_block.moderation_subscription_id
|
||||
else
|
||||
domain_block = DomainBlock.find_by(domain: @moderation_suggestion.target_key)
|
||||
UnblockDomainService.new.call(domain_block)
|
||||
# TODO: is this the way we want to log it?
|
||||
log_action :destroy, domain_block, moderation_subscription_id: domain_block.moderation_subscription_id
|
||||
end
|
||||
|
||||
@moderation_suggestion.mark_as_applied!
|
||||
end
|
||||
|
||||
redirect_to admin_moderation_suggestions_path, notice: I18n.t('admin.moderation_suggestions.retracted_msg', target_key: @moderation_suggestion.target_key)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def requires_confirmation?(domain_block)
|
||||
domain_block.valid? && (domain_block.new_record? || domain_block.severity_changed?) && domain_block.suspend? && !params[:confirm]
|
||||
end
|
||||
|
||||
def set_moderation_suggestion_targets
|
||||
@moderation_suggestion_targets = ModerationSuggestion.where(state: ['new', 'mailed']).reorder([target_type: :asc, target_key: :asc]).distinct.pluck(:target_type, :target_key)
|
||||
end
|
||||
|
||||
def set_moderation_suggestions_by_target
|
||||
@moderation_suggestions_by_target = begin
|
||||
@moderation_suggestion_targets.group_by(&:first).reduce(ModerationSuggestion.none) do |scope, (target_type, target_pairs)|
|
||||
scope.or(ModerationSuggestion.where(target_type: target_type, target_key: target_pairs.map(&:second)))
|
||||
end
|
||||
end.group_by { |suggestion| [suggestion.target_type, suggestion.target_key] }
|
||||
end
|
||||
|
||||
def set_moderation_advisories_by_target
|
||||
@moderation_advisories_by_target = begin
|
||||
@moderation_suggestion_targets.group_by(&:first).reduce(SubscribedAdvisory.none) do |scope, (target_type, target_pairs)|
|
||||
scope.or(SubscribedAdvisory.joins(:moderation_subscription).where(target_type: target_type, target_key: target_pairs.map(&:second)))
|
||||
end
|
||||
end.group_by { |advisory| [advisory.target_type, advisory.target_key] }
|
||||
|
||||
@moderation_advisories_by_target.each_value do |advisories|
|
||||
advisories.sort_by! { |advisory| advisory.moderation_subscription.priority }
|
||||
end
|
||||
|
||||
@moderation_advisories_by_target
|
||||
end
|
||||
|
||||
def set_current_state_by_target
|
||||
# TODO: handle other target types
|
||||
|
||||
if Rails.configuration.x.mastodon.limited_federation_mode
|
||||
@current_state_by_target = DomainAllow.where(domain: @moderation_suggestion_targets.filter_map { |type, key| key if type == 'domain' }).pluck(:domain).to_h { |domain| [['domain', domain], 'accept'] }
|
||||
@current_state_by_target.default = 'reject'
|
||||
else
|
||||
@current_state_by_target = DomainBlock.where(domain: @moderation_suggestion_targets.filter_map { |type, key| key if type == 'domain' }).to_h do |domain_block|
|
||||
action = begin
|
||||
case domain_block.severity
|
||||
when 'suspend'
|
||||
'reject'
|
||||
when 'silence'
|
||||
'limit'
|
||||
else
|
||||
'accept'
|
||||
end
|
||||
end
|
||||
|
||||
[['domain', domain_block.domain], action]
|
||||
end
|
||||
|
||||
@current_state_by_target.default = 'accept'
|
||||
end
|
||||
@current_state_by_target
|
||||
end
|
||||
|
||||
def set_moderation_suggestion
|
||||
@moderation_suggestion = ModerationSuggestion.find(params[:id])
|
||||
end
|
||||
end
|
||||
@@ -3,9 +3,9 @@
|
||||
module AccountableConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def log_action(action, target)
|
||||
def log_action(action, target, **)
|
||||
current_account
|
||||
.action_logs
|
||||
.create(action:, target:)
|
||||
.create(action:, target:, **)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,6 +39,8 @@ module Admin::ActionLogsHelper
|
||||
link_to log.human_identifier, admin_relays_path
|
||||
when 'Tag'
|
||||
link_to log.human_identifier, admin_tag_path(log.target_id)
|
||||
when 'ModerationSubscription'
|
||||
link_to log.human_identifier, admin_moderation_subscription_path(log.target_id)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -7,4 +7,18 @@ module Admin::ContentPoliciesHelper
|
||||
.map { |policy| I18n.t("admin.instances.content_policies.policies.#{policy}") }
|
||||
.join(' · ')
|
||||
end
|
||||
|
||||
def moderation_subscription_policy_list(moderation_subscription)
|
||||
policies = []
|
||||
|
||||
if moderation_subscription.apply_automatically?
|
||||
policies << (moderation_subscription.preserve_relationships? ? :apply_advisories_safely : :apply_advisories)
|
||||
end
|
||||
|
||||
policies << :apply_retractions if moderation_subscription.retract_automatically?
|
||||
|
||||
policies
|
||||
.map { |policy| I18n.t("admin.moderation_subscriptions.policies.#{policy}") }
|
||||
.join(' · ')
|
||||
end
|
||||
end
|
||||
|
||||
32
app/helpers/admin/moderation_subscriptions_helper.rb
Normal file
32
app/helpers/admin/moderation_subscriptions_helper.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin::ModerationSubscriptionsHelper
|
||||
def moderation_subscription_action_label(type)
|
||||
safe_join(
|
||||
[
|
||||
t("admin.moderation_subscriptions.list_actions.#{type}"),
|
||||
content_tag(:span, t("admin.moderation_subscriptions.list_actions_hint.#{type}"), class: 'hint'),
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def moderation_subscription_apply_conditions_label(type)
|
||||
safe_join(
|
||||
[
|
||||
t("admin.moderation_subscriptions.apply_conditions.#{type}"),
|
||||
content_tag(:span, t("admin.moderation_subscriptions.apply_conditions_hint.#{type}"), class: 'hint'),
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def moderation_subscription_badge(subscription)
|
||||
action = subscription.list_action || 'default'
|
||||
return unless %w(accept reject default).include?(action)
|
||||
|
||||
content_tag(
|
||||
:span,
|
||||
I18n.t("admin.moderation_subscriptions.list_actions_short.#{action}"),
|
||||
class: ['information-badge', action]
|
||||
)
|
||||
end
|
||||
end
|
||||
32
app/helpers/admin/moderation_suggestions_helper.rb
Normal file
32
app/helpers/admin/moderation_suggestions_helper.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin::ModerationSuggestionsHelper
|
||||
ACTION_ICON_NAMES = {
|
||||
accept: 'check',
|
||||
reject: 'block',
|
||||
limit: 'do_not_disturb_on',
|
||||
retract: 'backspace',
|
||||
}.freeze
|
||||
|
||||
def advisory_action_icon(action)
|
||||
material_symbol(ACTION_ICON_NAMES.fetch(action.to_sym))
|
||||
end
|
||||
|
||||
def advisory_action_link(action, suggestion)
|
||||
link_to(
|
||||
safe_join([advisory_action_icon(action), t("admin.moderation_suggestions.apply_#{action}")]),
|
||||
suggestion ? apply_admin_moderation_suggestion_path(suggestion) : nil,
|
||||
{ class: ['button', 'button-secondary', disabled: !suggestion], method: :post }
|
||||
)
|
||||
end
|
||||
|
||||
def suggestion_status_badge(current_state)
|
||||
return unless %w(accept limit reject).include?(current_state)
|
||||
|
||||
content_tag(
|
||||
:span,
|
||||
t("admin.moderation_suggestions.current_states.#{current_state}"),
|
||||
class: ['information-badge', current_state]
|
||||
)
|
||||
end
|
||||
end
|
||||
1
app/javascript/material-icons/400-24px/backspace.svg
Normal file
1
app/javascript/material-icons/400-24px/backspace.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m456-320 104-104 104 104 56-56-104-104 104-104-56-56-104 104-104-104-56 56 104 104-104 104 56 56Zm-96 160q-19 0-36-8.5T296-192L80-480l216-288q11-15 28-23.5t36-8.5h440q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H360ZM180-480l180 240h440v-480H360L180-480Zm400 0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 372 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M280-440h400v-80H280v80ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
||||
|
After Width: | Height: | Size: 415 B |
@@ -2511,3 +2511,45 @@ a.sparkline {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Temporary hacks for moderation subscriptions interface, rework after design pass
|
||||
|
||||
.information-badge {
|
||||
&.accept {
|
||||
color: var(--color-text-success);
|
||||
background-color: var(--color-bg-success-softest);
|
||||
border-color: var(--color-border-success-soft);
|
||||
}
|
||||
|
||||
&.reject {
|
||||
color: var(--color-text-error);
|
||||
background-color: var(--color-bg-error-softest);
|
||||
border-color: var(--color-border-error-soft);
|
||||
}
|
||||
|
||||
&.limit {
|
||||
color: var(--color-text-warning);
|
||||
background-color: var(--color-bg-warning-softest);
|
||||
border-color: var(--color-border-warning-soft);
|
||||
}
|
||||
}
|
||||
|
||||
.permissions-list__item__text__type a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.moderation_suggestion-retract {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.moderation_suggestion-limit {
|
||||
color: var(--color-text-warning);
|
||||
}
|
||||
|
||||
.moderation_suggestion-reject {
|
||||
color: var(--color-text-error);
|
||||
}
|
||||
|
||||
.permissions-list__item .table-action-link {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -40,4 +40,31 @@ class ModerationSubscription < ApplicationRecord
|
||||
accept: 0,
|
||||
reject: 1,
|
||||
}, suffix: :action
|
||||
|
||||
def to_log_human_identifier
|
||||
name
|
||||
end
|
||||
|
||||
def apply_conditions
|
||||
if apply_automatically?
|
||||
preserve_relationships? ? :safely : :always
|
||||
else
|
||||
:never
|
||||
end
|
||||
end
|
||||
|
||||
def apply_conditions=(conditions)
|
||||
case conditions.to_s
|
||||
when 'never'
|
||||
self.apply_automatically = false
|
||||
when 'always'
|
||||
self.apply_automatically = true
|
||||
self.preserve_relationships = false
|
||||
when 'safely'
|
||||
self.apply_automatically = true
|
||||
self.preserve_relationships = true
|
||||
else
|
||||
raise ArgumentError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,6 +36,23 @@ class ModerationSuggestion < ApplicationRecord
|
||||
|
||||
before_validation :normalize_target_key
|
||||
|
||||
def mark_as_applied!
|
||||
update!(state: :applied)
|
||||
ModerationSuggestion.where(target_type: target_type, target_key: target_key).where.not(id: id).update_all(state: :dismissed)
|
||||
end
|
||||
|
||||
def to_domain_allow
|
||||
raise ArgumentError unless target_type == 'domain' && action == 'accept'
|
||||
|
||||
DomainAllow.new(domain: target_key, moderation_subscription_id: moderation_subscription_id)
|
||||
end
|
||||
|
||||
def to_domain_block
|
||||
raise ArgumentError unless target_type == 'domain' && %w(limit reject).include?(action)
|
||||
|
||||
DomainBlock.new(domain: target_key, moderation_subscription_id: moderation_subscription_id, severity: action == 'reject' ? :suspend : :silence)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_target_key
|
||||
|
||||
23
app/policies/moderation_subscription_policy.rb
Normal file
23
app/policies/moderation_subscription_policy.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ModerationSubscriptionPolicy < ApplicationPolicy
|
||||
def index?
|
||||
role.can?(:manage_federation)
|
||||
end
|
||||
|
||||
def show?
|
||||
role.can?(:manage_federation)
|
||||
end
|
||||
|
||||
def create?
|
||||
role.can?(:manage_federation)
|
||||
end
|
||||
|
||||
def update?
|
||||
role.can?(:manage_federation)
|
||||
end
|
||||
|
||||
def destroy?
|
||||
role.can?(:manage_federation)
|
||||
end
|
||||
end
|
||||
19
app/policies/moderation_suggestion_policy.rb
Normal file
19
app/policies/moderation_suggestion_policy.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ModerationSuggestionPolicy < ApplicationPolicy
|
||||
def index?
|
||||
role.can?(:manage_federation)
|
||||
end
|
||||
|
||||
def show?
|
||||
role.can?(:manage_federation)
|
||||
end
|
||||
|
||||
def dismiss?
|
||||
role.can?(:manage_federation)
|
||||
end
|
||||
|
||||
def apply?
|
||||
role.can?(:manage_federation)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
.directory__tag
|
||||
= link_to admin_instance_path(advisory.target_key) do
|
||||
%h4
|
||||
= advisory.target_key
|
||||
|
||||
%small= t('admin.moderation_subscriptions.advisory_action', action: t("admin.moderation_subscriptions.advisory_actions.#{advisory.action}"))
|
||||
50
app/views/admin/moderation_subscriptions/_form.html.haml
Normal file
50
app/views/admin/moderation_subscriptions/_form.html.haml
Normal file
@@ -0,0 +1,50 @@
|
||||
.fields-group
|
||||
= form.input :name,
|
||||
label: t('admin.moderation_subscriptions.name'),
|
||||
wrapper: :with_label
|
||||
|
||||
.fields-group
|
||||
= form.input :url,
|
||||
label: t('admin.moderation_subscriptions.url'),
|
||||
wrapper: :with_label
|
||||
|
||||
.fields-group
|
||||
= form.input :priority,
|
||||
label: t('admin.moderation_subscriptions.priority'),
|
||||
hint: t('admin.moderation_subscriptions.priority_hint'),
|
||||
wrapper: :with_label
|
||||
|
||||
.fields-group
|
||||
= form.input :list_action,
|
||||
as: :radio_buttons,
|
||||
collection: [nil, 'accept', 'reject'],
|
||||
collection_wrapper_tag: 'ul',
|
||||
item_wrapper_tag: 'li',
|
||||
label_method: ->(type) { moderation_subscription_action_label(type || 'default') },
|
||||
label: t('admin.moderation_subscriptions.list_action'),
|
||||
wrapper: :with_label
|
||||
|
||||
.fields-group
|
||||
= form.input :apply_conditions,
|
||||
as: :radio_buttons,
|
||||
collection: %w(never safely always),
|
||||
collection_wrapper_tag: 'ul',
|
||||
item_wrapper_tag: 'li',
|
||||
include_blank: false,
|
||||
label_method: ->(type) { moderation_subscription_apply_conditions_label(type) },
|
||||
label: t('admin.moderation_subscriptions.apply_automatically'),
|
||||
wrapper: :with_label
|
||||
|
||||
.fields-group
|
||||
= form.input :override_unattributed,
|
||||
as: :boolean,
|
||||
wrapper: :with_label,
|
||||
label: t('admin.moderation_subscriptions.override_unattributed'),
|
||||
hint: t('admin.moderation_subscriptions.override_unattributed_hint')
|
||||
|
||||
.fields-group
|
||||
= form.input :retract_automatically,
|
||||
as: :boolean,
|
||||
wrapper: :with_label,
|
||||
label: t('admin.moderation_subscriptions.retract_automatically'),
|
||||
hint: t('admin.moderation_subscriptions.retract_automatically_hint')
|
||||
@@ -0,0 +1,10 @@
|
||||
.applications-list__item
|
||||
= link_to admin_moderation_subscription_path(moderation_subscription), class: 'announcements-list__item__title' do
|
||||
= moderation_subscription.name
|
||||
= moderation_subscription_badge(moderation_subscription)
|
||||
|
||||
.announcements-list__item__permissions
|
||||
.announcements-list__item__meta
|
||||
= moderation_subscription.url
|
||||
.announcements-list__item__meta
|
||||
= moderation_subscription_policy_list(moderation_subscription)
|
||||
12
app/views/admin/moderation_subscriptions/edit.html.haml
Normal file
12
app/views/admin/moderation_subscriptions/edit.html.haml
Normal file
@@ -0,0 +1,12 @@
|
||||
- content_for :page_title do
|
||||
= I18n.t('admin.moderation_subscriptions.show.title', name: @moderation_subscription.name)
|
||||
|
||||
= simple_form_for [:admin, @moderation_subscription] do |form|
|
||||
= render 'shared/error_messages', object: @moderation_subscription
|
||||
|
||||
= render form
|
||||
|
||||
.actions
|
||||
= form.button :button,
|
||||
t('generic.save_changes'),
|
||||
type: :submit
|
||||
20
app/views/admin/moderation_subscriptions/index.html.haml
Normal file
20
app/views/admin/moderation_subscriptions/index.html.haml
Normal file
@@ -0,0 +1,20 @@
|
||||
- content_for(:page_title) do
|
||||
= t('admin.moderation_subscriptions.title')
|
||||
|
||||
- content_for(:heading) do
|
||||
.content__heading__row
|
||||
%h1= yield :page_title
|
||||
|
||||
.content__heading__actions
|
||||
= link_to t('admin.moderation_subscriptions.add_new'), new_admin_moderation_subscription_path, class: 'button', id: 'add-subscription-button' if can?(:create, :moderation_subscription)
|
||||
|
||||
= render 'admin/moderation_subscriptions/shared/links'
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
- if @moderation_subscriptions.empty?
|
||||
.muted-hint.center-text
|
||||
= t 'admin.moderation_subscriptions.empty'
|
||||
- else
|
||||
.applications-list
|
||||
= render partial: 'moderation_subscription', collection: @moderation_subscriptions
|
||||
16
app/views/admin/moderation_subscriptions/logs.html.haml
Normal file
16
app/views/admin/moderation_subscriptions/logs.html.haml
Normal file
@@ -0,0 +1,16 @@
|
||||
- content_for(:heading) do
|
||||
.content__heading__row
|
||||
%h1= t('admin.moderation_subscriptions_logs.title')
|
||||
|
||||
= render 'admin/moderation_subscriptions/shared/links'
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
- if @action_logs.empty?
|
||||
.muted-hint.center-text
|
||||
= t 'admin.action_logs.empty'
|
||||
- else
|
||||
.report-notes
|
||||
= render partial: 'admin/action_logs/action_log', collection: @action_logs
|
||||
|
||||
= paginate @action_logs
|
||||
12
app/views/admin/moderation_subscriptions/new.html.haml
Normal file
12
app/views/admin/moderation_subscriptions/new.html.haml
Normal file
@@ -0,0 +1,12 @@
|
||||
- content_for :page_title do
|
||||
= t('admin.moderation_subscriptions.new.title')
|
||||
|
||||
= simple_form_for [:admin, @moderation_subscription] do |form|
|
||||
= render 'shared/error_messages', object: @moderation_subscription
|
||||
|
||||
= render form
|
||||
|
||||
.actions
|
||||
= form.button :button,
|
||||
t('.create'),
|
||||
type: :submit
|
||||
@@ -0,0 +1,6 @@
|
||||
.content__heading__tabs
|
||||
= render_navigation renderer: :links do |primary|
|
||||
:ruby
|
||||
primary.item :suggestions, safe_join([material_symbol('flag'), t('admin.moderation_subscriptions.tabs.suggestions')]), admin_moderation_suggestions_path
|
||||
primary.item :subscriptions, safe_join([material_symbol('edit'), t('admin.moderation_subscriptions.tabs.subscriptions')]), admin_moderation_subscriptions_path
|
||||
primary.item :logs, safe_join([material_symbol('list'), t('admin.moderation_subscriptions.tabs.logs')]), logs_admin_moderation_subscriptions_path
|
||||
53
app/views/admin/moderation_subscriptions/show.html.haml
Normal file
53
app/views/admin/moderation_subscriptions/show.html.haml
Normal file
@@ -0,0 +1,53 @@
|
||||
- content_for :page_title do
|
||||
= I18n.t('admin.moderation_subscriptions.show.title', name: @moderation_subscription.name)
|
||||
|
||||
- content_for :heading_actions do
|
||||
- if can?(:destroy, @moderation_subscription)
|
||||
= link_to t('admin.moderation_subscriptions.delete'),
|
||||
admin_moderation_subscription_path(@moderation_subscription),
|
||||
method: :delete,
|
||||
data: { confirm: t('admin.moderation_subscriptions.show.delete_confirm') },
|
||||
class: 'button button--destructive'
|
||||
|
||||
.table-wrapper
|
||||
%table.table.inline-table
|
||||
%tbody
|
||||
%tr
|
||||
%th= t('admin.moderation_subscriptions.priority')
|
||||
%td= @moderation_subscription.priority
|
||||
%tr
|
||||
%th= t('admin.moderation_subscriptions.type')
|
||||
%td= t("admin.moderation_subscriptions.types.#{@moderation_subscription.type}")
|
||||
%tr
|
||||
%th= t('admin.moderation_subscriptions.url')
|
||||
%td= @moderation_subscription.url
|
||||
%tr
|
||||
%th= t('admin.moderation_subscriptions.list_action')
|
||||
%td= t("admin.moderation_subscriptions.list_actions.#{@moderation_subscription.list_action || 'default'}")
|
||||
%tr
|
||||
%th= t('admin.moderation_subscriptions.apply_automatically')
|
||||
%td= t("admin.moderation_subscriptions.apply_conditions.#{@moderation_subscription.apply_conditions}")
|
||||
%tr
|
||||
%th= t('admin.moderation_subscriptions.override_unattributed')
|
||||
%td= @moderation_subscription.override_unattributed? ? t('simple_form.yes') : t('simple_form.no')
|
||||
%tr
|
||||
%th= t('admin.moderation_subscriptions.retract_automatically')
|
||||
%td= @moderation_subscription.retract_automatically? ? t('admin.moderation_subscriptions.apply_conditions.always') : t('admin.moderation_subscriptions.apply_conditions.never')
|
||||
- if @moderation_subscription.last_synced_at.present?
|
||||
%tr
|
||||
%th= t('admin.moderation_subscriptions.last_synced_at')
|
||||
%td
|
||||
%time.formatted{ datetime: @moderation_subscription.last_synced_at.iso8601 }= @moderation_subscription.last_synced_at
|
||||
|
||||
.action-buttons
|
||||
= link_to t('admin.moderation_subscriptions.show.edit'), edit_admin_moderation_subscription_path(@moderation_subscription), class: 'button' if can?(:update, @moderation_subscription)
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
- if @subscribed_advisories.empty?
|
||||
.muted-hint.center-text
|
||||
= t('admin.moderation_subscriptions.show.empty')
|
||||
- else
|
||||
= render partial: 'advisory', collection: @subscribed_advisories
|
||||
|
||||
= paginate @subscribed_advisories
|
||||
@@ -0,0 +1,38 @@
|
||||
.applications-list__item
|
||||
- target_type, target_key = moderation_suggestion_target
|
||||
- suggestions = moderation_suggestions_by_target[[target_type, target_key]]
|
||||
|
||||
= link_to admin_instance_path(target_key), class: 'announcements-list__item__title' do
|
||||
= target_key
|
||||
= suggestion_status_badge(current_state_by_target[[target_type, target_key]])
|
||||
|
||||
.announcements-list__item__action-bar
|
||||
.announcements-list__item__meta
|
||||
= t('admin.moderation_suggestions.description')
|
||||
|
||||
.announcements-list__item__actions
|
||||
= table_link_to 'close', t('admin.moderation_suggestions.dismiss'), admin_moderation_suggestion_path(suggestions.first), method: :delete
|
||||
|
||||
.announcements-list__item__permissions
|
||||
%ul.permissions-list
|
||||
- actions = (moderation_advisories_by_target[[target_type, target_key]] || []).pluck(:action).uniq
|
||||
- actions = ['retract'] + actions if suggestions&.any? { |suggestion| suggestion.action == 'retract' }
|
||||
- actions.map do |action|
|
||||
:ruby
|
||||
advisories = moderation_advisories_by_target.fetch([target_type, target_key], []).filter { |advisory| advisory.action == action }
|
||||
moderation_suggestion = suggestions&.find { |suggestion| suggestion.action == action }
|
||||
subscriptions = (advisories.map(&:moderation_subscription) + [moderation_suggestion&.moderation_subscription]).compact.uniq
|
||||
|
||||
%li.permissions-list__item
|
||||
.permissions-list__item__icon{ class: "moderation_suggestion-#{action}" }
|
||||
= advisory_action_icon(action)
|
||||
.permissions-list__item__text
|
||||
.permissions-list__item__text__title
|
||||
= t(action, scope: 'admin.moderation_suggestions.actions')
|
||||
.permissions-list__item__text__type
|
||||
= t('admin.moderation_suggestions.suggested_from_html', link: to_sentence(subscriptions.map { |subscription| link_to(subscription.name, admin_moderation_subscription_path(subscription)) }))
|
||||
- if moderation_suggestion && can?(:apply, moderation_suggestion)
|
||||
= table_link_to 'check',
|
||||
t("admin.moderation_suggestions.apply_#{moderation_suggestion.action}"),
|
||||
apply_admin_moderation_suggestion_path(moderation_suggestion),
|
||||
method: :post
|
||||
@@ -0,0 +1,13 @@
|
||||
- content_for :page_title do
|
||||
= t('admin.domain_blocks.confirm_downgrade.title', domain: Addressable::IDNA.to_unicode(@moderation_suggestion.target_key))
|
||||
|
||||
= simple_form_for @moderation_suggestion, url: apply_admin_moderation_suggestion_path, method: :post do |f|
|
||||
%p.hint= t('admin.domain_blocks.confirm_downgrade.preamble_html', domain: Addressable::IDNA.to_unicode(@moderation_suggestion.target_key))
|
||||
|
||||
= f.hidden_field :id
|
||||
|
||||
%hr.spacer
|
||||
|
||||
.actions
|
||||
= link_to t('admin.domain_blocks.confirm_downgrade.cancel'), admin_moderation_suggestions_path, class: 'button button-secondary'
|
||||
= f.button :submit, t('admin.domain_blocks.confirm_downgrade.confirm'), class: 'button button--dangerous', name: :confirm_downgrade
|
||||
@@ -0,0 +1,21 @@
|
||||
- content_for :page_title do
|
||||
= t('admin.domain_blocks.confirm_suspension.title', domain: Addressable::IDNA.to_unicode(@moderation_suggestion.target_key))
|
||||
|
||||
= simple_form_for @moderation_suggestion, url: apply_admin_moderation_suggestion_path, method: :post do |f|
|
||||
%p.hint= t('admin.domain_blocks.confirm_suspension.preamble_html', domain: Addressable::IDNA.to_unicode(@moderation_suggestion.target_key))
|
||||
%ul.hint
|
||||
%li= t('admin.domain_blocks.confirm_suspension.stop_communication')
|
||||
%li= t('admin.domain_blocks.confirm_suspension.remove_all_data')
|
||||
%li= t('admin.domain_blocks.confirm_suspension.undo_relationships')
|
||||
%li.negative-hint= t('admin.domain_blocks.confirm_suspension.permanent_action')
|
||||
|
||||
= f.hidden_field :id
|
||||
|
||||
%hr.spacer
|
||||
|
||||
= react_admin_component :impact_report,
|
||||
domain: @moderation_suggestion.target_key
|
||||
|
||||
.actions
|
||||
= link_to t('admin.domain_blocks.confirm_suspension.cancel'), admin_moderation_suggestions_path, class: 'button button-secondary'
|
||||
= f.button :submit, t('admin.domain_blocks.confirm_suspension.confirm'), class: 'button button--dangerous', name: :confirm
|
||||
19
app/views/admin/moderation_suggestions/index.html.haml
Normal file
19
app/views/admin/moderation_suggestions/index.html.haml
Normal file
@@ -0,0 +1,19 @@
|
||||
- content_for :page_title do
|
||||
= t('admin.moderation_suggestions.title')
|
||||
|
||||
- content_for(:heading) do
|
||||
.content__heading__row
|
||||
%h1= yield :page_title
|
||||
|
||||
= render 'admin/moderation_subscriptions/shared/links'
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
- if @moderation_suggestion_targets.empty?
|
||||
.muted-hint.center-text
|
||||
= t 'admin.moderation_suggestions.empty'
|
||||
- else
|
||||
.applications-list
|
||||
= render partial: 'moderation_suggestion_target',
|
||||
collection: @moderation_suggestion_targets,
|
||||
locals: { moderation_advisories_by_target: @moderation_advisories_by_target, moderation_suggestions_by_target: @moderation_suggestions_by_target, current_state_by_target: @current_state_by_target }
|
||||
@@ -279,6 +279,7 @@ en:
|
||||
create_domain_block_html: "%{name} blocked domain %{target}"
|
||||
create_email_domain_block_html: "%{name} blocked email domain %{target}"
|
||||
create_ip_block_html: "%{name} created rule for IP %{target}"
|
||||
create_moderation_subscription_html: "%{name} created moderation subscription %{target}"
|
||||
create_relay_html: "%{name} created a relay %{target}"
|
||||
create_unavailable_domain_html: "%{name} stopped delivery to domain %{target}"
|
||||
create_user_role_html: "%{name} created %{target} role"
|
||||
@@ -293,6 +294,7 @@ en:
|
||||
destroy_email_domain_block_html: "%{name} unblocked email domain %{target}"
|
||||
destroy_instance_html: "%{name} purged domain %{target}"
|
||||
destroy_ip_block_html: "%{name} deleted rule for IP %{target}"
|
||||
destroy_moderation_subscription_html: "%{name} deleted moderation subscription %{target}"
|
||||
destroy_relay_html: "%{name} deleted the relay %{target}"
|
||||
destroy_status_html: "%{name} removed post by %{target}"
|
||||
destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}"
|
||||
@@ -342,6 +344,7 @@ en:
|
||||
update_username_block_html: "%{name} updated rule for usernames containing %{target}"
|
||||
usable_tag_html: can be used;
|
||||
deleted_account: deleted account
|
||||
deleted_suggestion: a deleted moderation subscription
|
||||
empty: No logs found.
|
||||
filter_by_action: Filter by action
|
||||
filter_by_user: Filter by user
|
||||
@@ -460,6 +463,11 @@ en:
|
||||
undo: Disallow federation with domain
|
||||
domain_blocks:
|
||||
add_new: Add new domain block
|
||||
confirm_downgrade:
|
||||
cancel: Cancel
|
||||
confirm: Downgrade
|
||||
preamble_html: You are about to downgrade the existing block on <strong>%{domain}</strong> and its subdomains.
|
||||
title: Confirm domain block downgrade for %{domain}
|
||||
confirm_suspension:
|
||||
cancel: Cancel
|
||||
confirm: Suspend
|
||||
@@ -758,6 +766,92 @@ en:
|
||||
reset: Reset
|
||||
search: Search
|
||||
title: IP rules
|
||||
moderation_subscriptions:
|
||||
add_new: Add subscription
|
||||
advisory_action: 'Action: %{action}'
|
||||
advisory_actions:
|
||||
accept: allow
|
||||
limit: silence
|
||||
reject: suspend
|
||||
apply_automatically: Apply advisories automatically
|
||||
apply_conditions:
|
||||
always: Always
|
||||
never: Never
|
||||
safely: Only if it does not break relationships
|
||||
apply_conditions_hint:
|
||||
always: Always apply recommended actions automatically. Only use this option for lists you fully trust.
|
||||
never: Never apply recommended actions automatically. Always generate a suggestion instead.
|
||||
safely: Apply recommended actions unless they would break existing follow relationships with local users. In such cases, a suggestion will be generated instead.
|
||||
delete: Delete
|
||||
destroyed_msg: Successfully deleted moderation subscription
|
||||
empty: No moderation list has been subscribed to yet.
|
||||
last_synced_at: Last synced at
|
||||
list_action: Subscription type
|
||||
list_actions:
|
||||
accept: Allow-list
|
||||
default: Mixed (Allow & Deny)
|
||||
reject: Block-list
|
||||
list_actions_hint:
|
||||
accept: Items with no specified action will be treated as allowed. Blocks will be ignored.
|
||||
default: Items with no specified action will be treated as blocks. Blocks and allows will both be treated as such.
|
||||
reject: Items with no specified action will be treated as blocks. Allows will be ignored.
|
||||
list_actions_short:
|
||||
accept: Allow-list
|
||||
default: Mixed
|
||||
reject: Block-list
|
||||
name: Name
|
||||
new:
|
||||
create: Subscribe
|
||||
title: New moderation subscription
|
||||
override_unattributed: Override manual blocks
|
||||
override_unattributed_hint: Allow this subscription to automatically upgrade or take over existing manual blocks when automatically applying advisories.
|
||||
policies:
|
||||
apply_advisories: Automatically applies advisories
|
||||
apply_advisories_safely: Automatically applies advisories that do not break relationships
|
||||
apply_retractions: Automatically applies retractions
|
||||
priority: Priority
|
||||
priority_hint: Moderation subscriptions are evaluated in order of priority, subscriptions with a lower number override subscriptions with a higher one.
|
||||
retract_automatically: Retract advisories automatically
|
||||
retract_automatically_hint: Whether to allow retracting or downgrading policy changes when the subscription removes a corresponding item. When automatic retraction is enabled and an equivalent advisory exists in a lower-priority list, it will be adopted. When automatic retraction is disabled, a suggestion will be generated instead.
|
||||
show:
|
||||
delete_confirm: Are you sure you want to delete this moderation subscription?
|
||||
edit: Edit subscription
|
||||
empty: This subscription has no advisory yet.
|
||||
title: 'Moderation subscription: %{name}'
|
||||
tabs:
|
||||
logs: Logs
|
||||
subscriptions: Subscriptions
|
||||
suggestions: Suggestions
|
||||
title: Moderation subscriptions
|
||||
type: Type
|
||||
types:
|
||||
csv_list: CSV list
|
||||
url: URL
|
||||
moderation_subscriptions_logs:
|
||||
title: Moderation subscriptions logs
|
||||
moderation_suggestions:
|
||||
actions:
|
||||
accept: Allow
|
||||
limit: Limit
|
||||
reject: Suspend
|
||||
retract: Retract current policy
|
||||
applied_msg: Successfully applied suggestion for %{target_key}
|
||||
apply_accept: Allow
|
||||
apply_limit: Limit
|
||||
apply_reject: Suspend
|
||||
apply_retract: Retract
|
||||
current_states:
|
||||
accept: currently allowed
|
||||
limit: currently limited
|
||||
reject: currently suspended
|
||||
description: Below are suggestions from your moderation subscriptions, ordered by priority.
|
||||
destroyed_msg: Successfully dismissed suggestion for %{target_key}
|
||||
dismiss: Dismiss suggestions
|
||||
empty: There is no moderation suggestion at this time.
|
||||
retracted_msg: Successfully retracted policy for %{target_key}
|
||||
suggested_from_html: Suggested by %{link}
|
||||
title: Moderation suggestions
|
||||
unable_to_apply_msg: Unable to apply suggestion for %{target_key}
|
||||
relationships:
|
||||
title: "%{acct}'s relationships"
|
||||
relays:
|
||||
|
||||
@@ -62,6 +62,7 @@ SimpleNavigation::Configuration.run do |navigation|
|
||||
s.item :instances, safe_join([material_symbol('cloud'), t('admin.instances.title')]), admin_instances_path(limited: limited_federation_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows|/admin/export_domain_blocks}, if: lambda {
|
||||
current_user.can?(:manage_federation)
|
||||
}
|
||||
s.item :moderation_subscriptions, safe_join([material_symbol('cloud_download'), t('admin.moderation_subscriptions.title')]), admin_moderation_suggestions_path, highlights_on: %r{/admin/moderation_subscriptions|/admin/moderation_suggestions}, if: -> { current_user.can?(:manage_federation) }
|
||||
s.item :email_domain_blocks, safe_join([material_symbol('mail'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_path, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.can?(:manage_blocks) }
|
||||
s.item :username_blocks, safe_join([material_symbol('supervised_user_circle_off'), t('admin.username_blocks.title')]), admin_username_blocks_path, highlights_on: %r{/admin/username_blocks}, if: -> { current_user.can?(:manage_blocks) }
|
||||
s.item :ip_blocks, safe_join([material_symbol('hide_source'), t('admin.ip_blocks.title')]), admin_ip_blocks_path, highlights_on: %r{/admin/ip_blocks}, if: -> { current_user.can?(:manage_blocks) }
|
||||
|
||||
@@ -110,6 +110,18 @@ namespace :admin do
|
||||
resources :moderation_notes, module: :instances, only: [:create, :destroy]
|
||||
end
|
||||
|
||||
resources :moderation_subscriptions do
|
||||
collection do
|
||||
get :logs
|
||||
end
|
||||
end
|
||||
|
||||
resources :moderation_suggestions do
|
||||
member do
|
||||
post :apply
|
||||
end
|
||||
end
|
||||
|
||||
resources :rules, only: [:index, :new, :create, :edit, :update, :destroy] do
|
||||
member do
|
||||
post :move_up
|
||||
|
||||
56
spec/system/admin/moderation_subscriptions_spec.rb
Normal file
56
spec/system/admin/moderation_subscriptions_spec.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'using the moderation subscription interface' do
|
||||
before do
|
||||
sign_in Fabricate(:admin_user), scope: :user
|
||||
end
|
||||
|
||||
context 'with a new subscription' do
|
||||
it 'allows saving it, editing it and deleting it' do
|
||||
visit admin_moderation_subscriptions_path
|
||||
expect(page)
|
||||
.to have_title(I18n.t('admin.moderation_subscriptions.title'))
|
||||
|
||||
# Navigate to new subscription form
|
||||
click_on I18n.t('admin.moderation_subscriptions.add_new')
|
||||
expect(page)
|
||||
.to have_title(I18n.t('admin.moderation_subscriptions.new.title'))
|
||||
|
||||
# Submitting while leaving out some required fields
|
||||
fill_in 'moderation_subscription_name', with: 'My first blocklist'
|
||||
expect { click_on I18n.t('admin.moderation_subscriptions.new.create') }
|
||||
.to_not change(ModerationSubscription, :count)
|
||||
expect(page)
|
||||
.to have_text(/errors below/)
|
||||
|
||||
# Filling in the missing fields and submitting
|
||||
fill_in 'moderation_subscription_url', with: 'https://example.org/blocklist.csv'
|
||||
fill_in 'moderation_subscription_priority', with: 0
|
||||
expect { click_on I18n.t('admin.moderation_subscriptions.new.create') }
|
||||
.to change(ModerationSubscription, :count).by(1)
|
||||
|
||||
expect(page)
|
||||
.to have_title(I18n.t('admin.moderation_subscriptions.title'))
|
||||
|
||||
expect(ModerationSubscription.find_by(name: 'My first blocklist'))
|
||||
.to have_attributes(url: 'https://example.org/blocklist.csv')
|
||||
|
||||
# The new subscription is displayed and can be visited
|
||||
click_on 'My first blocklist'
|
||||
expect(page)
|
||||
.to have_title(I18n.t('admin.moderation_subscriptions.show.title', name: 'My first blocklist'))
|
||||
|
||||
# The subscription can be edited
|
||||
click_on I18n.t('admin.moderation_subscriptions.show.edit')
|
||||
fill_in 'moderation_subscription_priority', with: 10
|
||||
expect { click_on I18n.t('generic.save_changes') }
|
||||
.to change { ModerationSubscription.find_by(name: 'My first blocklist').priority }.from(0).to(10)
|
||||
|
||||
# The subscription can be deleted
|
||||
expect { click_on I18n.t('admin.moderation_subscriptions.delete') }
|
||||
.to change(ModerationSubscription, :count).by(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user