Add appeals (#17364)
* Add appeals * Add ability to reject appeals and ability to browse pending appeals in admin UI * Add strikes to account page in settings * Various fixes and improvements - Add separate notification setting for appeals, separate from reports - Fix style of links in report/strike header - Change approving an appeal to not restore statuses (due to federation complexities) - Change style of successfully appealed strikes on account settings page - Change account settings page to only show unappealed or recently appealed strikes * Change appealed_at to overruled_at * Fix missing method errormain
parent
5be705e1e0
commit
564efd0651
@ -0,0 +1,40 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Disputes::AppealsController < Admin::BaseController
|
||||||
|
before_action :set_appeal, except: :index
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :appeal, :index?
|
||||||
|
|
||||||
|
@appeals = filtered_appeals.page(params[:page])
|
||||||
|
end
|
||||||
|
|
||||||
|
def approve
|
||||||
|
authorize @appeal, :approve?
|
||||||
|
log_action :approve, @appeal
|
||||||
|
ApproveAppealService.new.call(@appeal, current_account)
|
||||||
|
redirect_to disputes_strike_path(@appeal.strike)
|
||||||
|
end
|
||||||
|
|
||||||
|
def reject
|
||||||
|
authorize @appeal, :approve?
|
||||||
|
log_action :reject, @appeal
|
||||||
|
@appeal.reject!(current_account)
|
||||||
|
UserMailer.appeal_rejected(@appeal.account.user, @appeal)
|
||||||
|
redirect_to disputes_strike_path(@appeal.strike)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def filtered_appeals
|
||||||
|
Admin::AppealFilter.new(filter_params.with_defaults(status: 'pending')).results.includes(strike: :account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
params.slice(:page, *Admin::AppealFilter::KEYS).permit(:page, *Admin::AppealFilter::KEYS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_appeal
|
||||||
|
@appeal = Appeal.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Disputes::AppealsController < Disputes::BaseController
|
||||||
|
before_action :set_strike
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize @strike, :appeal?
|
||||||
|
|
||||||
|
@appeal = AppealService.new.call(@strike, appeal_params[:text])
|
||||||
|
|
||||||
|
redirect_to disputes_strike_path(@strike), notice: I18n.t('disputes.strikes.appealed_msg')
|
||||||
|
rescue ActiveRecord::RecordInvalid
|
||||||
|
render template: 'disputes/strikes/show'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_strike
|
||||||
|
@strike = current_account.strikes.find(params[:strike_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def appeal_params
|
||||||
|
params.require(:appeal).permit(:text)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Disputes::BaseController < ApplicationController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
|
layout 'admin'
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
before_action :set_body_classes
|
||||||
|
before_action :authenticate_user!
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_body_classes
|
||||||
|
@body_classes = 'admin'
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Disputes::StrikesController < Disputes::BaseController
|
||||||
|
before_action :set_strike
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize @strike, :show?
|
||||||
|
|
||||||
|
@appeal = @strike.appeal || @strike.build_appeal
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_strike
|
||||||
|
@strike = AccountWarning.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin::Trends::StatusesHelper
|
||||||
|
def one_line_preview(status)
|
||||||
|
text = begin
|
||||||
|
if status.local?
|
||||||
|
status.text.split("\n").first
|
||||||
|
else
|
||||||
|
Nokogiri::HTML(status.text).css('html > body > *').first&.text
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return '' if text.blank?
|
||||||
|
|
||||||
|
html = Formatter.instance.send(:encode, text)
|
||||||
|
html = Formatter.instance.send(:encode_custom_emojis, html, status.emojis, prefers_autoplay?)
|
||||||
|
|
||||||
|
html.html_safe # rubocop:disable Rails/OutputSafety
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,49 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::AppealFilter
|
||||||
|
KEYS = %i(
|
||||||
|
status
|
||||||
|
).freeze
|
||||||
|
|
||||||
|
attr_reader :params
|
||||||
|
|
||||||
|
def initialize(params)
|
||||||
|
@params = params
|
||||||
|
end
|
||||||
|
|
||||||
|
def results
|
||||||
|
scope = Appeal.order(id: :desc)
|
||||||
|
|
||||||
|
params.each do |key, value|
|
||||||
|
next if %w(page).include?(key.to_s)
|
||||||
|
|
||||||
|
scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
scope
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def scope_for(key, value)
|
||||||
|
case key.to_s
|
||||||
|
when 'status'
|
||||||
|
status_scope(value)
|
||||||
|
else
|
||||||
|
raise "Unknown filter: #{key}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def status_scope(value)
|
||||||
|
case value
|
||||||
|
when 'approved'
|
||||||
|
Appeal.approved
|
||||||
|
when 'rejected'
|
||||||
|
Appeal.rejected
|
||||||
|
when 'pending'
|
||||||
|
Appeal.pending
|
||||||
|
else
|
||||||
|
raise "Unknown status: #{value}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,58 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: appeals
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# account_id :bigint(8) not null
|
||||||
|
# account_warning_id :bigint(8) not null
|
||||||
|
# text :text default(""), not null
|
||||||
|
# approved_at :datetime
|
||||||
|
# approved_by_account_id :bigint(8)
|
||||||
|
# rejected_at :datetime
|
||||||
|
# rejected_by_account_id :bigint(8)
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
class Appeal < ApplicationRecord
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :strike, class_name: 'AccountWarning', foreign_key: 'account_warning_id'
|
||||||
|
belongs_to :approved_by_account, class_name: 'Account', optional: true
|
||||||
|
belongs_to :rejected_by_account, class_name: 'Account', optional: true
|
||||||
|
|
||||||
|
validates :text, presence: true, length: { maximum: 2_000 }
|
||||||
|
validates :account_warning_id, uniqueness: true
|
||||||
|
|
||||||
|
validate :validate_time_frame, on: :create
|
||||||
|
|
||||||
|
scope :approved, -> { where.not(approved_at: nil) }
|
||||||
|
scope :rejected, -> { where.not(rejected_at: nil) }
|
||||||
|
scope :pending, -> { where(approved_at: nil, rejected_at: nil) }
|
||||||
|
|
||||||
|
def pending?
|
||||||
|
!approved? && !rejected?
|
||||||
|
end
|
||||||
|
|
||||||
|
def approved?
|
||||||
|
approved_at.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def rejected?
|
||||||
|
rejected_at.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def approve!(current_account)
|
||||||
|
update!(approved_at: Time.now.utc, approved_by_account: current_account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def reject!(current_account)
|
||||||
|
update!(rejected_at: Time.now.utc, rejected_by_account: current_account)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def validate_time_frame
|
||||||
|
errors.add(:base, I18n.t('strikes.errors.too_late')) if Time.now.utc > (strike.created_at + 20.days)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AccountWarningPolicy < ApplicationPolicy
|
||||||
|
def show?
|
||||||
|
target? || staff?
|
||||||
|
end
|
||||||
|
|
||||||
|
def appeal?
|
||||||
|
target?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def target?
|
||||||
|
record.target_account_id == current_account&.id
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AppealPolicy < ApplicationPolicy
|
||||||
|
def index?
|
||||||
|
staff?
|
||||||
|
end
|
||||||
|
|
||||||
|
def approve?
|
||||||
|
record.pending? && staff?
|
||||||
|
end
|
||||||
|
|
||||||
|
alias reject? approve?
|
||||||
|
end
|
@ -0,0 +1,28 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AppealService < BaseService
|
||||||
|
def call(strike, text)
|
||||||
|
@strike = strike
|
||||||
|
@text = text
|
||||||
|
|
||||||
|
create_appeal!
|
||||||
|
notify_staff!
|
||||||
|
|
||||||
|
@appeal
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_appeal!
|
||||||
|
@appeal = @strike.create_appeal!(
|
||||||
|
text: @text,
|
||||||
|
account: @strike.target_account
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def notify_staff!
|
||||||
|
User.staff.includes(:account).each do |u|
|
||||||
|
AdminMailer.new_appeal(u.account, @appeal).deliver_later if u.allows_appeal_emails?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,74 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ApproveAppealService < BaseService
|
||||||
|
def call(appeal, current_account)
|
||||||
|
@appeal = appeal
|
||||||
|
@strike = appeal.strike
|
||||||
|
@current_account = current_account
|
||||||
|
|
||||||
|
ApplicationRecord.transaction do
|
||||||
|
undo_strike_action!
|
||||||
|
mark_strike_as_appealed!
|
||||||
|
end
|
||||||
|
|
||||||
|
queue_workers!
|
||||||
|
notify_target_account!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def target_account
|
||||||
|
@strike.target_account
|
||||||
|
end
|
||||||
|
|
||||||
|
def undo_strike_action!
|
||||||
|
case @strike.action
|
||||||
|
when 'disable'
|
||||||
|
undo_disable!
|
||||||
|
when 'delete_statuses'
|
||||||
|
undo_delete_statuses!
|
||||||
|
when 'sensitive'
|
||||||
|
undo_sensitive!
|
||||||
|
when 'silence'
|
||||||
|
undo_silence!
|
||||||
|
when 'suspend'
|
||||||
|
undo_suspend!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def mark_strike_as_appealed!
|
||||||
|
@appeal.approve!(@current_account)
|
||||||
|
@strike.touch(:overruled_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
def undo_disable!
|
||||||
|
target_account.user.enable!
|
||||||
|
end
|
||||||
|
|
||||||
|
def undo_delete_statuses!
|
||||||
|
# Cannot be undone
|
||||||
|
end
|
||||||
|
|
||||||
|
def undo_sensitive!
|
||||||
|
target_account.unsensitize!
|
||||||
|
end
|
||||||
|
|
||||||
|
def undo_silence!
|
||||||
|
target_account.unsilence!
|
||||||
|
end
|
||||||
|
|
||||||
|
def undo_suspend!
|
||||||
|
target_account.unsuspend!
|
||||||
|
end
|
||||||
|
|
||||||
|
def queue_workers!
|
||||||
|
case @strike.action
|
||||||
|
when 'suspend'
|
||||||
|
Admin::UnsuspensionWorker.perform_async(target_account.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def notify_target_account!
|
||||||
|
UserMailer.appeal_approved(target_account.user, @appeal).deliver_later
|
||||||
|
end
|
||||||
|
end
|
@ -1,7 +0,0 @@
|
|||||||
.speech-bubble
|
|
||||||
.speech-bubble__bubble
|
|
||||||
= simple_format(h(account_moderation_note.content))
|
|
||||||
.speech-bubble__owner
|
|
||||||
= admin_account_link_to account_moderation_note.account
|
|
||||||
%time.formatted{ datetime: account_moderation_note.created_at.iso8601 }= l account_moderation_note.created_at
|
|
||||||
= table_link_to 'trash', t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete if can?(:destroy, account_moderation_note)
|
|
@ -1,6 +1,24 @@
|
|||||||
.speech-bubble.warning
|
= link_to disputes_strike_path(account_warning), class: ['log-entry', account_warning.overruled? && 'log-entry--inactive'] do
|
||||||
.speech-bubble__bubble
|
.log-entry__header
|
||||||
= Formatter.instance.linkify(account_warning.text)
|
.log-entry__avatar
|
||||||
.speech-bubble__owner
|
= image_tag account_warning.target_account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar'
|
||||||
= admin_account_link_to account_warning.account
|
.log-entry__content
|
||||||
%time.formatted{ datetime: account_warning.created_at.iso8601 }= l account_warning.created_at
|
.log-entry__title
|
||||||
|
= t(account_warning.action, scope: 'admin.strikes.actions', name: content_tag(:span, account_warning.account.username, class: 'username'), target: content_tag(:span, account_warning.target_account.acct, class: 'target')).html_safe
|
||||||
|
.log-entry__timestamp
|
||||||
|
%time.formatted{ datetime: account_warning.created_at.iso8601 }
|
||||||
|
= l(account_warning.created_at)
|
||||||
|
|
||||||
|
- if account_warning.report_id.present?
|
||||||
|
·
|
||||||
|
= t('admin.reports.title', id: account_warning.report_id)
|
||||||
|
|
||||||
|
- if account_warning.overruled?
|
||||||
|
·
|
||||||
|
%span.positive-hint= t('admin.strikes.appeal_approved')
|
||||||
|
- elsif account_warning.appeal&.pending?
|
||||||
|
·
|
||||||
|
%span.warning-hint= t('admin.strikes.appeal_pending')
|
||||||
|
- elsif account_warning.appeal&.rejected?
|
||||||
|
·
|
||||||
|
%span.negative-hint= t('admin.strikes.appeal_rejected')
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
= link_to disputes_strike_path(appeal.strike), class: ['log-entry', appeal.approved? && 'log-entry--inactive'] do
|
||||||
|
.log-entry__header
|
||||||
|
.log-entry__avatar
|
||||||
|
= image_tag appeal.account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar'
|
||||||
|
.log-entry__content
|
||||||
|
.log-entry__title
|
||||||
|
= t(appeal.strike.action, scope: 'admin.strikes.actions', name: content_tag(:span, appeal.strike.account.username, class: 'username'), target: content_tag(:span, appeal.account.acct, class: 'target')).html_safe
|
||||||
|
.log-entry__timestamp
|
||||||
|
%time.formatted{ datetime: appeal.strike.created_at.iso8601 }
|
||||||
|
= l(appeal.strike.created_at)
|
||||||
|
|
||||||
|
- if appeal.strike.report_id.present?
|
||||||
|
·
|
||||||
|
= t('admin.reports.title', id: appeal.strike.report_id)
|
||||||
|
·
|
||||||
|
- if appeal.approved?
|
||||||
|
%span.positive-hint= t('admin.strikes.appeal_approved')
|
||||||
|
- elsif appeal.rejected?
|
||||||
|
%span.negative-hint= t('admin.strikes.appeal_rejected')
|
||||||
|
- else
|
||||||
|
%span.warning-hint= t('admin.strikes.appeal_pending')
|
@ -0,0 +1,22 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.disputes.appeals.title')
|
||||||
|
|
||||||
|
- content_for :header_tags do
|
||||||
|
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
|
||||||
|
|
||||||
|
.filters
|
||||||
|
.filter-subset
|
||||||
|
%strong= t('admin.tags.review')
|
||||||
|
%ul
|
||||||
|
%li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{Appeal.pending.count})"], ' '), status: 'pending'
|
||||||
|
%li= filter_link_to t('admin.trends.approved'), status: 'approved'
|
||||||
|
%li= filter_link_to t('admin.trends.rejected'), status: 'rejected'
|
||||||
|
|
||||||
|
- if @appeals.empty?
|
||||||
|
%div.muted-hint.center-text
|
||||||
|
= t 'admin.disputes.appeals.empty'
|
||||||
|
- else
|
||||||
|
.announcements-list
|
||||||
|
= render partial: 'appeal', collection: @appeals
|
||||||
|
|
||||||
|
= paginate @appeals
|
@ -0,0 +1,9 @@
|
|||||||
|
<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
|
||||||
|
|
||||||
|
<%= raw t('admin_mailer.new_appeal.body', target: @appeal.account.username, action_taken_by: @appeal.strike.account.username, date: l(@appeal.strike.created_at), type: t(@appeal.strike.action, scope: 'admin_mailer.new_appeal.actions')) %>
|
||||||
|
|
||||||
|
> <%= raw word_wrap(@appeal.text, break_sequence: "\n> ") %>
|
||||||
|
|
||||||
|
<%= raw t('admin_mailer.new_appeal.next_steps') %>
|
||||||
|
|
||||||
|
<%= raw t('application_mailer.view')%> <%= disputes_strike_url(@appeal.strike) %>
|
@ -0,0 +1,20 @@
|
|||||||
|
= link_to disputes_strike_path(account_warning), class: 'log-entry' do
|
||||||
|
.log-entry__header
|
||||||
|
.log-entry__avatar
|
||||||
|
.indicator-icon{ class: account_warning.overruled? ? 'success' : 'failure' }
|
||||||
|
= fa_icon 'warning'
|
||||||
|
.log-entry__content
|
||||||
|
.log-entry__title
|
||||||
|
= t('disputes.strikes.title', action: t(account_warning.action, scope: 'disputes.strikes.title_actions'), date: l(account_warning.created_at.to_date))
|
||||||
|
.log-entry__timestamp
|
||||||
|
%time.formatted{ datetime: account_warning.created_at.iso8601 }= l(account_warning.created_at)
|
||||||
|
|
||||||
|
- if account_warning.overruled?
|
||||||
|
·
|
||||||
|
%span.positive-hint= t('disputes.strikes.your_appeal_approved')
|
||||||
|
- elsif account_warning.appeal&.pending?
|
||||||
|
·
|
||||||
|
%span.warning-hint= t('disputes.strikes.your_appeal_pending')
|
||||||
|
- elsif account_warning.appeal&.rejected?
|
||||||
|
·
|
||||||
|
%span.negative-hint= t('disputes.strikes.your_appeal_rejected')
|
@ -1,22 +1,17 @@
|
|||||||
|
- if !@user.confirmed?
|
||||||
|
.flash-message.warning
|
||||||
|
= t('auth.status.confirming')
|
||||||
|
= link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path
|
||||||
|
- elsif !@user.approved?
|
||||||
|
.flash-message.warning
|
||||||
|
= t('auth.status.pending')
|
||||||
|
- elsif @user.account.moved_to_account_id.present?
|
||||||
|
.flash-message.warning
|
||||||
|
= t('auth.status.redirecting_to', acct: @user.account.moved_to_account.acct)
|
||||||
|
= link_to t('migrations.cancel'), settings_migration_path
|
||||||
|
|
||||||
%h3= t('auth.status.account_status')
|
%h3= t('auth.status.account_status')
|
||||||
|
|
||||||
.simple_form
|
= render partial: 'account_warning', collection: @strikes
|
||||||
%p.hint
|
|
||||||
- if @user.account.suspended?
|
|
||||||
%span.negative-hint= t('user_mailer.warning.explanation.suspend')
|
|
||||||
- elsif @user.disabled?
|
|
||||||
%span.negative-hint= t('user_mailer.warning.explanation.disable')
|
|
||||||
- elsif @user.account.silenced?
|
|
||||||
%span.warning-hint= t('user_mailer.warning.explanation.silence')
|
|
||||||
- elsif !@user.confirmed?
|
|
||||||
%span.warning-hint= t('auth.status.confirming')
|
|
||||||
= link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path
|
|
||||||
- elsif !@user.approved?
|
|
||||||
%span.warning-hint= t('auth.status.pending')
|
|
||||||
- elsif @user.account.moved_to_account_id.present?
|
|
||||||
%span.positive-hint= t('auth.status.redirecting_to', acct: @user.account.moved_to_account.acct)
|
|
||||||
= link_to t('migrations.cancel'), settings_migration_path
|
|
||||||
- else
|
|
||||||
%span.positive-hint= t('auth.status.functional')
|
|
||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
@ -0,0 +1,127 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('disputes.strikes.title', action: t(@strike.action, scope: 'disputes.strikes.title_actions'), date: l(@strike.created_at.to_date))
|
||||||
|
|
||||||
|
- content_for :heading_actions do
|
||||||
|
- if @appeal.persisted?
|
||||||
|
= link_to t('admin.accounts.approve'), approve_admin_disputes_appeal_path(@appeal), method: :post, class: 'button' if can?(:approve, @appeal)
|
||||||
|
= link_to t('admin.accounts.reject'), reject_admin_disputes_appeal_path(@appeal), method: :post, class: 'button button--destructive' if can?(:reject, @appeal)
|
||||||
|
|
||||||
|
- if @strike.overruled?
|
||||||
|
%p.hint
|
||||||
|
%span.positive-hint
|
||||||
|
= fa_icon 'check'
|
||||||
|
= ' '
|
||||||
|
= t 'disputes.strikes.appeal_approved'
|
||||||
|
- elsif @appeal.persisted? && @appeal.rejected?
|
||||||
|
%p.hint
|
||||||
|
%span.negative-hint
|
||||||
|
= fa_icon 'times'
|
||||||
|
= ' '
|
||||||
|
= t 'disputes.strikes.appeal_rejected'
|
||||||
|
|
||||||
|
.report-header
|
||||||
|
.report-header__card
|
||||||
|
.strike-card
|
||||||
|
- unless @strike.none_action?
|
||||||
|
%p= t "user_mailer.warning.explanation.#{@strike.action}"
|
||||||
|
|
||||||
|
- unless @strike.text.blank?
|
||||||
|
= Formatter.instance.linkify(@strike.text)
|
||||||
|
|
||||||
|
- if @strike.report && !@strike.report.other?
|
||||||
|
%p
|
||||||
|
%strong= t('user_mailer.warning.reason')
|
||||||
|
= t("user_mailer.warning.categories.#{@strike.report.category}")
|
||||||
|
|
||||||
|
- if @strike.report.violation? && @strike.report.rule_ids.present?
|
||||||
|
%ul.rules-list
|
||||||
|
- @strike.report.rules.each do |rule|
|
||||||
|
%li= rule.text
|
||||||
|
|
||||||
|
- if @strike.status_ids.present? && !@strike.status_ids.empty?
|
||||||
|
%p
|
||||||
|
%strong= t('user_mailer.warning.statuses')
|
||||||
|
|
||||||
|
.strike-card__statuses-list
|
||||||
|
- status_map = @strike.statuses.includes(:application, :media_attachments).index_by(&:id)
|
||||||
|
|
||||||
|
- @strike.status_ids.each do |status_id|
|
||||||
|
.strike-card__statuses-list__item
|
||||||
|
- if (status = status_map[status_id.to_i])
|
||||||
|
.one-liner
|
||||||
|
= link_to short_account_status_url(@strike.target_account, status_id), class: 'emojify' do
|
||||||
|
= one_line_preview(status)
|
||||||
|
|
||||||
|
- status.media_attachments.each do |media_attachment|
|
||||||
|
%abbr{ title: media_attachment.description }
|
||||||
|
= fa_icon 'link'
|
||||||
|
= media_attachment.file_file_name
|
||||||
|
.strike-card__statuses-list__item__meta
|
||||||
|
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
||||||
|
·
|
||||||
|
= status.application.name
|
||||||
|
- else
|
||||||
|
.one-liner= t('disputes.strikes.status', id: status_id)
|
||||||
|
.strike-card__statuses-list__item__meta
|
||||||
|
= t('disputes.strikes.status_removed')
|
||||||
|
|
||||||
|
.report-header__details
|
||||||
|
.report-header__details__item
|
||||||
|
.report-header__details__item__header
|
||||||
|
%strong= t('disputes.strikes.created_at')
|
||||||
|
.report-header__details__item__content
|
||||||
|
%time.formatted{ datetime: @strike.created_at.iso8601, title: l(@strike.created_at) }= l(@strike.created_at)
|
||||||
|
.report-header__details__item
|
||||||
|
.report-header__details__item__header
|
||||||
|
%strong= t('disputes.strikes.recipient')
|
||||||
|
.report-header__details__item__content
|
||||||
|
= admin_account_link_to @strike.target_account, path: can?(:show, @strike.target_account) ? admin_account_path(@strike.target_account_id) : ActivityPub::TagManager.instance.url_for(@strike.target_account)
|
||||||
|
.report-header__details__item
|
||||||
|
.report-header__details__item__header
|
||||||
|
%strong= t('disputes.strikes.action_taken')
|
||||||
|
.report-header__details__item__content
|
||||||
|
- if @strike.overruled?
|
||||||
|
%del= t(@strike.action, scope: 'user_mailer.warning.title')
|
||||||
|
- else
|
||||||
|
= t(@strike.action, scope: 'user_mailer.warning.title')
|
||||||
|
- if @strike.report && can?(:show, @strike.report)
|
||||||
|
.report-header__details__item
|
||||||
|
.report-header__details__item__header
|
||||||
|
%strong= t('disputes.strikes.associated_report')
|
||||||
|
.report-header__details__item__content
|
||||||
|
= link_to t('admin.reports.report', id: @strike.report.id), admin_report_path(@strike.report)
|
||||||
|
- if @appeal.persisted?
|
||||||
|
.report-header__details__item
|
||||||
|
.report-header__details__item__header
|
||||||
|
%strong= t('disputes.strikes.appeal_submitted_at')
|
||||||
|
.report-header__details__item__content
|
||||||
|
%time.formatted{ datetime: @appeal.created_at.iso8601, title: l(@appeal.created_at) }= l(@appeal.created_at)
|
||||||
|
%hr.spacer/
|
||||||
|
|
||||||
|
- if @appeal.persisted?
|
||||||
|
%h3= t('disputes.strikes.appeal')
|
||||||
|
|
||||||
|
.report-notes
|
||||||
|
.report-notes__item
|
||||||
|
= image_tag @appeal.account.avatar.url, class: 'report-notes__item__avatar'
|
||||||
|
|
||||||
|
.report-notes__item__header
|
||||||
|
%span.username
|
||||||
|
= link_to @appeal.account.username, can?(:show, @appeal.account) ? admin_account_path(@appeal.account_id) : short_account_url(@appeal.account)
|
||||||
|
%time{ datetime: @appeal.created_at.iso8601, title: l(@appeal.created_at) }
|
||||||
|
- if @appeal.created_at.today?
|
||||||
|
= t('admin.report_notes.today_at', time: l(@appeal.created_at, format: :time))
|
||||||
|
- else
|
||||||
|
= l @appeal.created_at.to_date
|
||||||
|
|
||||||
|
.report-notes__item__content
|
||||||
|
= simple_format(h(@appeal.text))
|
||||||
|
- elsif can?(:appeal, @strike)
|
||||||
|
%h3= t('disputes.strikes.appeals.submit')
|
||||||
|
|
||||||
|
= simple_form_for(@appeal, url: disputes_strike_appeal_path(@strike)) do |f|
|
||||||
|
.fields-group
|
||||||
|
= f.input :text, wrapper: :with_label, input_html: { maxlength: 500 }
|
||||||
|
|
||||||
|
.actions
|
||||||
|
= f.button :button, t('disputes.strikes.appeals.submit'), type: :submit
|
@ -0,0 +1,59 @@
|
|||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.hero
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center.padded
|
||||||
|
%table.hero-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td
|
||||||
|
= image_tag full_pack_url('media/images/mailer/icon_done.png'), alt: ''
|
||||||
|
|
||||||
|
%h1= t 'user_mailer.appeal_approved.title'
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.content-start
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center
|
||||||
|
%p= t 'user_mailer.appeal_approved.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at)
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.button-cell
|
||||||
|
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.button-primary
|
||||||
|
= link_to root_url do
|
||||||
|
%span= t 'user_mailer.appeal_approved.action'
|
@ -0,0 +1,7 @@
|
|||||||
|
<%= t 'user_mailer.appeal_approved.title' %>
|
||||||
|
|
||||||
|
===
|
||||||
|
|
||||||
|
<%= t 'user_mailer.appeal_approved.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at) %>
|
||||||
|
|
||||||
|
=> <%= root_url %>
|
@ -0,0 +1,59 @@
|
|||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.hero
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center.padded
|
||||||
|
%table.hero-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td
|
||||||
|
= image_tag full_pack_url('media/images/mailer/icon_warning.png'), alt: ''
|
||||||
|
|
||||||
|
%h1= t 'user_mailer.appeal_rejected.title'
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.content-start
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center
|
||||||
|
%p= t 'user_mailer.appeal_rejected.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at)
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.button-cell
|
||||||
|
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.button-primary
|
||||||
|
= link_to root_url do
|
||||||
|
%span= t 'user_mailer.appeal_approved.action'
|
@ -0,0 +1,7 @@
|
|||||||
|
<%= t 'user_mailer.appeal_rejected.title' %>
|
||||||
|
|
||||||
|
===
|
||||||
|
|
||||||
|
<%= t 'user_mailer.appeal_rejected.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at) %>
|
||||||
|
|
||||||
|
=> <%= root_url %>
|
@ -0,0 +1,14 @@
|
|||||||
|
class CreateAppeals < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table :appeals do |t|
|
||||||
|
t.belongs_to :account, null: false, foreign_key: { on_delete: :cascade }
|
||||||
|
t.belongs_to :account_warning, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true }
|
||||||
|
t.text :text, null: false, default: ''
|
||||||
|
t.datetime :approved_at
|
||||||
|
t.belongs_to :approved_by_account, foreign_key: { to_table: :accounts, on_delete: :nullify }
|
||||||
|
t.datetime :rejected_at
|
||||||
|
t.belongs_to :rejected_by_account, foreign_key: { to_table: :accounts, on_delete: :nullify }
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,5 @@
|
|||||||
|
class AddOverruledAtToAccountWarnings < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :account_warnings, :overruled_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,53 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Admin::Disputes::AppealsController, type: :controller do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
before { sign_in current_user, scope: :user }
|
||||||
|
|
||||||
|
let(:target_account) { Fabricate(:account) }
|
||||||
|
let(:strike) { Fabricate(:account_warning, target_account: target_account, action: :suspend) }
|
||||||
|
let(:appeal) { Fabricate(:appeal, strike: strike, account: target_account) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
target_account.suspend!
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST #approve' do
|
||||||
|
let(:current_user) { Fabricate(:user, admin: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(UserMailer).to receive(:appeal_approved).and_return(double('email', deliver_later: nil))
|
||||||
|
post :approve, params: { id: appeal.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'unsuspends a suspended account' do
|
||||||
|
expect(target_account.reload.suspended?).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects back to the strike page' do
|
||||||
|
expect(response).to redirect_to(disputes_strike_path(appeal.strike))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'notifies target account about approved appeal' do
|
||||||
|
expect(UserMailer).to have_received(:appeal_approved).with(target_account.user, appeal)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST #reject' do
|
||||||
|
let(:current_user) { Fabricate(:user, admin: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(UserMailer).to receive(:appeal_rejected).and_return(double('email', deliver_later: nil))
|
||||||
|
post :reject, params: { id: appeal.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects back to the strike page' do
|
||||||
|
expect(response).to redirect_to(disputes_strike_path(appeal.strike))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'notifies target account about rejected appeal' do
|
||||||
|
expect(UserMailer).to have_received(:appeal_rejected).with(target_account.user, appeal)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,27 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Disputes::AppealsController, type: :controller do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
before { sign_in current_user, scope: :user }
|
||||||
|
|
||||||
|
let!(:admin) { Fabricate(:user, admin: true) }
|
||||||
|
|
||||||
|
describe '#create' do
|
||||||
|
let(:current_user) { Fabricate(:user) }
|
||||||
|
let(:strike) { Fabricate(:account_warning, target_account: current_user.account) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(AdminMailer).to receive(:new_appeal).and_return(double('email', deliver_later: nil))
|
||||||
|
post :create, params: { strike_id: strike.id, appeal: { text: 'Foo' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'notifies staff about new appeal' do
|
||||||
|
expect(AdminMailer).to have_received(:new_appeal).with(admin.account, Appeal.last)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects back to the strike page' do
|
||||||
|
expect(response).to redirect_to(disputes_strike_path(strike.id))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,30 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Disputes::StrikesController, type: :controller do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
before { sign_in current_user, scope: :user }
|
||||||
|
|
||||||
|
describe '#show' do
|
||||||
|
let(:current_user) { Fabricate(:user) }
|
||||||
|
let(:strike) { Fabricate(:account_warning, target_account: current_user.account) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
get :show, params: { id: strike.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when meant for the user' do
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when meant for a different user' do
|
||||||
|
let(:strike) { Fabricate(:account_warning) }
|
||||||
|
|
||||||
|
it 'returns http forbidden' do
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,5 +1,6 @@
|
|||||||
Fabricator(:account_warning) do
|
Fabricator(:account_warning) do
|
||||||
account nil
|
account
|
||||||
target_account nil
|
target_account(fabricator: :account)
|
||||||
text "MyText"
|
text { Faker::Lorem.paragraph }
|
||||||
|
action 'suspend'
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
Fabricator(:appeal) do
|
||||||
|
strike(fabricator: :account_warning)
|
||||||
|
account { |attrs| attrs[:strike].target_account }
|
||||||
|
text { Faker::Lorem.paragraph }
|
||||||
|
end
|
@ -0,0 +1,5 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Appeal, type: :model do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
Loading…
Reference in new issue