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 error
This commit is contained in:
parent
5be705e1e0
commit
564efd0651
60 changed files with 1212 additions and 93 deletions
|
@ -28,7 +28,7 @@ module Admin
|
|||
@deletion_request = @account.deletion_request
|
||||
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||
@warnings = @account.strikes.custom.latest
|
||||
@warnings = @account.strikes.includes(:target_account, :account, :appeal).latest
|
||||
@domain_block = DomainBlock.rule_for(@account.domain)
|
||||
end
|
||||
|
||||
|
@ -146,7 +146,7 @@ module Admin
|
|||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
|
||||
params.slice(:page, *AccountFilter::KEYS).permit(:page, *AccountFilter::KEYS)
|
||||
end
|
||||
|
||||
def form_account_batch_params
|
||||
|
|
|
@ -8,6 +8,7 @@ module Admin
|
|||
@pending_users_count = User.pending.count
|
||||
@pending_reports_count = Report.unresolved.count
|
||||
@pending_tags_count = Tag.pending_review.count
|
||||
@pending_appeals_count = Appeal.pending.count
|
||||
end
|
||||
|
||||
private
|
||||
|
|
40
app/controllers/admin/disputes/appeals_controller.rb
Normal file
40
app/controllers/admin/disputes/appeals_controller.rb
Normal file
|
@ -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
|
|
@ -9,6 +9,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
before_action :check_enabled_registrations, only: [:new, :create]
|
||||
before_action :configure_sign_up_params, only: [:create]
|
||||
before_action :set_sessions, only: [:edit, :update]
|
||||
before_action :set_strikes, only: [:edit, :update]
|
||||
before_action :set_instance_presenter, only: [:new, :create, :update]
|
||||
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
||||
before_action :require_not_suspended!, only: [:update]
|
||||
|
@ -111,8 +112,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
end
|
||||
|
||||
def set_invite
|
||||
invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil
|
||||
@invite = invite&.valid_for_use? ? invite : nil
|
||||
@invite = begin
|
||||
invite = Invite.find_by(code: invite_code) if invite_code.present?
|
||||
invite if invite&.valid_for_use?
|
||||
end
|
||||
end
|
||||
|
||||
def determine_layout
|
||||
|
@ -123,6 +126,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
@sessions = current_user.session_activations
|
||||
end
|
||||
|
||||
def set_strikes
|
||||
@strikes = current_account.strikes.active.latest
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
|
|
25
app/controllers/disputes/appeals_controller.rb
Normal file
25
app/controllers/disputes/appeals_controller.rb
Normal file
|
@ -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
|
18
app/controllers/disputes/base_controller.rb
Normal file
18
app/controllers/disputes/base_controller.rb
Normal file
|
@ -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
|
17
app/controllers/disputes/strikes_controller.rb
Normal file
17
app/controllers/disputes/strikes_controller.rb
Normal file
|
@ -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
|
|
@ -1,10 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin::AccountModerationNotesHelper
|
||||
def admin_account_link_to(account)
|
||||
def admin_account_link_to(account, path: nil)
|
||||
return if account.nil?
|
||||
|
||||
link_to admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do
|
||||
link_to path || admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do
|
||||
safe_join([
|
||||
image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'),
|
||||
content_tag(:span, account.acct, class: 'username'),
|
||||
|
|
|
@ -33,6 +33,8 @@ module Admin::ActionLogsHelper
|
|||
"#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
|
||||
when 'Instance'
|
||||
record.domain
|
||||
when 'Appeal'
|
||||
link_to record.account.acct, disputes_strike_path(record.strike)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
20
app/helpers/admin/trends/statuses_helper.rb
Normal file
20
app/helpers/admin/trends/statuses_helper.rb
Normal file
|
@ -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
|
|
@ -578,12 +578,16 @@ body,
|
|||
}
|
||||
|
||||
.log-entry {
|
||||
display: block;
|
||||
line-height: 20px;
|
||||
padding: 15px;
|
||||
padding-left: 15px * 2 + 40px;
|
||||
background: $ui-base-color;
|
||||
border-bottom: 1px solid darken($ui-base-color, 8%);
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
color: $darker-text-color;
|
||||
font-size: 14px;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
|
@ -596,15 +600,12 @@ body,
|
|||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
}
|
||||
|
||||
&__header {
|
||||
color: $darker-text-color;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
|
@ -640,6 +641,18 @@ body,
|
|||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&--inactive {
|
||||
.log-entry__title {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
a,
|
||||
.username,
|
||||
.target {
|
||||
color: $darker-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.name-tag,
|
||||
|
@ -1175,6 +1188,17 @@ a.sparkline {
|
|||
font-weight: 600;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $ui-highlight-color;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--horizontal {
|
||||
|
@ -1451,3 +1475,56 @@ a.sparkline {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.strike-card {
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
background: $ui-base-color;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
word-wrap: break-word;
|
||||
font-weight: 400;
|
||||
color: $primary-text-color;
|
||||
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
unicode-bidi: plaintext;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__statuses-list {
|
||||
border-radius: 4px;
|
||||
border: 1px solid darken($ui-base-color, 8%);
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
overflow: hidden;
|
||||
|
||||
&__item {
|
||||
padding: 16px;
|
||||
background: lighten($ui-base-color, 2%);
|
||||
border-bottom: 1px solid darken($ui-base-color, 8%);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
color: $darker-text-color;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,16 @@ class AdminMailer < ApplicationMailer
|
|||
end
|
||||
end
|
||||
|
||||
def new_appeal(recipient, appeal)
|
||||
@appeal = appeal
|
||||
@me = recipient
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
locale_for_account(@me) do
|
||||
mail to: @me.user_email, subject: I18n.t('admin_mailer.new_appeal.subject', instance: @instance, username: @appeal.account.username)
|
||||
end
|
||||
end
|
||||
|
||||
def new_pending_account(recipient, user)
|
||||
@account = user.account
|
||||
@me = recipient
|
||||
|
|
|
@ -173,6 +173,26 @@ class UserMailer < Devise::Mailer
|
|||
end
|
||||
end
|
||||
|
||||
def appeal_approved(user, appeal)
|
||||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
@appeal = appeal
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('user_mailer.appeal_approved.subject', date: l(@appeal.created_at))
|
||||
end
|
||||
end
|
||||
|
||||
def appeal_rejected(user, appeal)
|
||||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
@appeal = appeal
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('user_mailer.appeal_rejected.subject', date: l(@appeal.created_at))
|
||||
end
|
||||
end
|
||||
|
||||
def sign_in_token(user, remote_ip, user_agent, timestamp)
|
||||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
|
|
@ -270,6 +270,10 @@ class Account < ApplicationRecord
|
|||
true
|
||||
end
|
||||
|
||||
def previous_strikes_count
|
||||
strikes.where(overruled_at: nil).count
|
||||
end
|
||||
|
||||
def keypair
|
||||
@keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
|
||||
end
|
||||
|
|
|
@ -24,6 +24,8 @@ class AccountFilter
|
|||
scope = Account.includes(:account_stat, user: [:ips, :invite_request]).without_instance_actor.reorder(nil)
|
||||
|
||||
params.each do |key, value|
|
||||
next if key.to_s == 'page'
|
||||
|
||||
scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
|
||||
end
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
# updated_at :datetime not null
|
||||
# report_id :bigint(8)
|
||||
# status_ids :string is an Array
|
||||
# overruled_at :datetime
|
||||
#
|
||||
|
||||
class AccountWarning < ApplicationRecord
|
||||
|
@ -28,12 +29,17 @@ class AccountWarning < ApplicationRecord
|
|||
belongs_to :target_account, class_name: 'Account', inverse_of: :strikes
|
||||
belongs_to :report, optional: true
|
||||
|
||||
has_one :appeal, dependent: :destroy
|
||||
has_one :appeal, dependent: :destroy, inverse_of: :strike
|
||||
|
||||
scope :latest, -> { order(id: :desc) }
|
||||
scope :custom, -> { where.not(text: '') }
|
||||
scope :active, -> { where(overruled_at: nil).or(where('account_warnings.overruled_at >= ?', 30.days.ago)) }
|
||||
|
||||
def statuses
|
||||
Status.with_discarded.where(id: status_ids || [])
|
||||
end
|
||||
|
||||
def overruled?
|
||||
overruled_at.present?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,8 @@ class Admin::ActionLogFilter
|
|||
).freeze
|
||||
|
||||
ACTION_TYPE_MAP = {
|
||||
approve_appeal: { target_type: 'Appeal', action: 'approve' }.freeze,
|
||||
reject_appeal: { target_type: 'Appeal', action: 'reject' }.freeze,
|
||||
assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze,
|
||||
change_email_user: { target_type: 'User', action: 'change_email' }.freeze,
|
||||
confirm_user: { target_type: 'User', action: 'confirm' }.freeze,
|
||||
|
|
49
app/models/admin/appeal_filter.rb
Normal file
49
app/models/admin/appeal_filter.rb
Normal file
|
@ -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
|
58
app/models/appeal.rb
Normal file
58
app/models/appeal.rb
Normal file
|
@ -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
|
|
@ -265,6 +265,10 @@ class User < ApplicationRecord
|
|||
settings.notification_emails['pending_account']
|
||||
end
|
||||
|
||||
def allows_appeal_emails?
|
||||
settings.notification_emails['appeal']
|
||||
end
|
||||
|
||||
def allows_trending_tag_emails?
|
||||
settings.notification_emails['trending_tag']
|
||||
end
|
||||
|
|
17
app/policies/account_warning_policy.rb
Normal file
17
app/policies/account_warning_policy.rb
Normal file
|
@ -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
|
13
app/policies/appeal_policy.rb
Normal file
13
app/policies/appeal_policy.rb
Normal file
|
@ -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
|
28
app/services/appeal_service.rb
Normal file
28
app/services/appeal_service.rb
Normal file
|
@ -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
|
74
app/services/approve_appeal_service.rb
Normal file
74
app/services/approve_appeal_service.rb
Normal file
|
@ -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
|
||||
.speech-bubble__bubble
|
||||
= Formatter.instance.linkify(account_warning.text)
|
||||
.speech-bubble__owner
|
||||
= admin_account_link_to account_warning.account
|
||||
%time.formatted{ datetime: account_warning.created_at.iso8601 }= l account_warning.created_at
|
||||
= link_to disputes_strike_path(account_warning), class: ['log-entry', account_warning.overruled? && 'log-entry--inactive'] do
|
||||
.log-entry__header
|
||||
.log-entry__avatar
|
||||
= image_tag account_warning.target_account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar'
|
||||
.log-entry__content
|
||||
.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')
|
||||
|
|
|
@ -246,18 +246,29 @@
|
|||
%hr.spacer/
|
||||
|
||||
- unless @warnings.empty?
|
||||
= render @warnings
|
||||
|
||||
%h3= t 'admin.accounts.previous_strikes'
|
||||
|
||||
%p= t('admin.accounts.previous_strikes_description_html', count: @account.previous_strikes_count)
|
||||
|
||||
.account-strikes
|
||||
= render @warnings
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
= render @moderation_notes
|
||||
%h3= t 'admin.reports.notes.title'
|
||||
|
||||
%p= t 'admin.reports.notes_description_html'
|
||||
|
||||
.report-notes
|
||||
= render partial: 'admin/report_notes/report_note', collection: @moderation_notes
|
||||
|
||||
= simple_form_for @account_moderation_note, url: admin_account_moderation_notes_path do |f|
|
||||
= render 'shared/error_messages', object: @account_moderation_note
|
||||
|
||||
= f.input :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6
|
||||
= f.hidden_field :target_account_id
|
||||
|
||||
.field-group
|
||||
= f.input :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6
|
||||
|
||||
.actions
|
||||
= f.button :button, t('admin.account_moderation_notes.create'), type: :submit
|
||||
|
||||
|
|
|
@ -46,6 +46,9 @@
|
|||
%span= t('admin.dashboard.pending_tags_html', count: @pending_tags_count)
|
||||
= fa_icon 'chevron-right fw'
|
||||
|
||||
= link_to admin_disputes_appeals_path(status: 'pending'), class: 'dashboard__quick-access' do
|
||||
%span= t('admin.dashboard.pending_appeals_html', count: @pending_appeals_count)
|
||||
= fa_icon 'chevron-right fw'
|
||||
.dashboard__item
|
||||
= react_admin_component :dimension, dimension: 'sources', start_at: @time_period.first, end_at: @time_period.last, limit: 8, label: t('admin.dashboard.sources')
|
||||
|
||||
|
|
21
app/views/admin/disputes/appeals/_appeal.html.haml
Normal file
21
app/views/admin/disputes/appeals/_appeal.html.haml
Normal file
|
@ -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')
|
22
app/views/admin/disputes/appeals/index.html.haml
Normal file
22
app/views/admin/disputes/appeals/index.html.haml
Normal file
|
@ -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
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
.report-notes__item__header
|
||||
%span.username
|
||||
= link_to display_name(report_note.account), admin_account_path(report_note.account_id)
|
||||
= link_to report_note.account.username, admin_account_path(report_note.account_id)
|
||||
%time{ datetime: report_note.created_at.iso8601, title: l(report_note.created_at) }
|
||||
- if report_note.created_at.today?
|
||||
= t('admin.report_notes.today_at', time: l(report_note.created_at, format: :time))
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
.report-header__details__item__header
|
||||
%strong= t('admin.accounts.strikes')
|
||||
.report-header__details__item__content
|
||||
= @report.target_account.strikes.count
|
||||
= @report.target_account.previous_strikes_count
|
||||
|
||||
.report-header__details
|
||||
.report-header__details__item
|
||||
|
|
9
app/views/admin_mailer/new_appeal.text.erb
Normal file
9
app/views/admin_mailer/new_appeal.text.erb
Normal file
|
@ -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) %>
|
20
app/views/auth/registrations/_account_warning.html.haml
Normal file
20
app/views/auth/registrations/_account_warning.html.haml
Normal file
|
@ -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')
|
||||
|
||||
.simple_form
|
||||
%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')
|
||||
= render partial: 'account_warning', collection: @strikes
|
||||
|
||||
%hr.spacer/
|
||||
|
|
127
app/views/disputes/strikes/show.html.haml
Normal file
127
app/views/disputes/strikes/show.html.haml
Normal file
|
@ -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
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
- if current_user.staff?
|
||||
= ff.input :report, as: :boolean, wrapper: :with_label
|
||||
= ff.input :appeal, as: :boolean, wrapper: :with_label
|
||||
= ff.input :pending_account, as: :boolean, wrapper: :with_label
|
||||
= ff.input :trending_tag, as: :boolean, wrapper: :with_label
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
%span.detailed-status__visibility-icon
|
||||
= visibility_icon status
|
||||
·
|
||||
- if status.application && @account.user&.setting_show_application
|
||||
- if status.application && status.account.user&.setting_show_application
|
||||
- if status.application.website.blank?
|
||||
%strong.detailed-status__application= status.application.name
|
||||
- else
|
||||
|
|
59
app/views/user_mailer/appeal_approved.html.haml
Normal file
59
app/views/user_mailer/appeal_approved.html.haml
Normal file
|
@ -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'
|
7
app/views/user_mailer/appeal_approved.text.erb
Normal file
7
app/views/user_mailer/appeal_approved.text.erb
Normal file
|
@ -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 %>
|
59
app/views/user_mailer/appeal_rejected.html.haml
Normal file
59
app/views/user_mailer/appeal_rejected.html.haml
Normal file
|
@ -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'
|
7
app/views/user_mailer/appeal_rejected.text.erb
Normal file
7
app/views/user_mailer/appeal_rejected.text.erb
Normal file
|
@ -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 %>
|
|
@ -77,8 +77,8 @@
|
|||
%tbody
|
||||
%tr
|
||||
%td.button-primary
|
||||
= link_to about_more_url do
|
||||
%span= t 'user_mailer.warning.review_server_policies'
|
||||
= link_to disputes_strike_url(@warning) do
|
||||
%span= t 'user_mailer.warning.appeal'
|
||||
|
||||
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
|
@ -95,4 +95,4 @@
|
|||
%tbody
|
||||
%tr
|
||||
%td.column-cell.text-center
|
||||
%p= t 'user_mailer.warning.get_in_touch', instance: @instance
|
||||
%p= t 'user_mailer.warning.appeal_description', instance: @instance
|
||||
|
|
|
@ -1,25 +1,5 @@
|
|||
{
|
||||
"ignored_warnings": [
|
||||
{
|
||||
"warning_type": "SQL Injection",
|
||||
"warning_code": 0,
|
||||
"fingerprint": "04dbbc249b989db2e0119bbb0f59c9818e12889d2b97c529cdc0b1526002ba4b",
|
||||
"check_name": "SQL",
|
||||
"message": "Possible SQL injection",
|
||||
"file": "app/models/report.rb",
|
||||
"line": 113,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||
"code": "Admin::ActionLog.from(\"(#{[Admin::ActionLog.where(:target_type => \"Report\", :target_id => id, :created_at => ((created_at..updated_at))).unscope(:order), Admin::ActionLog.where(:target_type => \"Account\", :target_id => target_account_id, :created_at => ((created_at..updated_at))).unscope(:order), Admin::ActionLog.where(:target_type => \"Status\", :target_id => status_ids, :created_at => ((created_at..updated_at))).unscope(:order)].map do\n \"(#{query.to_sql})\"\n end.join(\" UNION ALL \")}) AS admin_action_logs\")",
|
||||
"render_path": null,
|
||||
"location": {
|
||||
"type": "method",
|
||||
"class": "Report",
|
||||
"method": "history"
|
||||
},
|
||||
"user_input": "Admin::ActionLog.where(:target_type => \"Status\", :target_id => status_ids, :created_at => ((created_at..updated_at))).unscope(:order)",
|
||||
"confidence": "High",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "SQL Injection",
|
||||
"warning_code": 0,
|
||||
|
@ -27,7 +7,7 @@
|
|||
"check_name": "SQL",
|
||||
"message": "Possible SQL injection",
|
||||
"file": "app/models/status.rb",
|
||||
"line": 100,
|
||||
"line": 104,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||
"code": "result.joins(\"INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}\")",
|
||||
"render_path": null,
|
||||
|
@ -107,7 +87,7 @@
|
|||
"check_name": "PermitAttributes",
|
||||
"message": "Potentially dangerous key allowed for mass assignment",
|
||||
"file": "app/controllers/api/v1/admin/reports_controller.rb",
|
||||
"line": 78,
|
||||
"line": 90,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
|
||||
"code": "params.permit(:resolved, :account_id, :target_account_id)",
|
||||
"render_path": null,
|
||||
|
@ -140,6 +120,36 @@
|
|||
"confidence": "Medium",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "Cross-Site Scripting",
|
||||
"warning_code": 2,
|
||||
"fingerprint": "afad51718ae373b2f19d2513029fd2afccf58b9148e475934bc6a162ee33c352",
|
||||
"check_name": "CrossSiteScripting",
|
||||
"message": "Unescaped model attribute",
|
||||
"file": "app/views/admin/disputes/appeals/_appeal.html.haml",
|
||||
"line": 7,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
|
||||
"code": "t((Unresolved Model).new.strike.action, :scope => \"admin.strikes.actions\", :name => content_tag(:span, (Unresolved Model).new.strike.account.username, :class => \"username\"), :target => content_tag(:span, (Unresolved Model).new.account.acct, :class => \"target\"))",
|
||||
"render_path": [
|
||||
{
|
||||
"type": "template",
|
||||
"name": "admin/disputes/appeals/index",
|
||||
"line": 16,
|
||||
"file": "app/views/admin/disputes/appeals/index.html.haml",
|
||||
"rendered": {
|
||||
"name": "admin/disputes/appeals/_appeal",
|
||||
"file": "app/views/admin/disputes/appeals/_appeal.html.haml"
|
||||
}
|
||||
}
|
||||
],
|
||||
"location": {
|
||||
"type": "template",
|
||||
"template": "admin/disputes/appeals/_appeal"
|
||||
},
|
||||
"user_input": "(Unresolved Model).new.strike",
|
||||
"confidence": "Weak",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "Redirect",
|
||||
"warning_code": 18,
|
||||
|
@ -194,7 +204,7 @@
|
|||
{
|
||||
"type": "template",
|
||||
"name": "admin/trends/links/index",
|
||||
"line": 37,
|
||||
"line": 39,
|
||||
"file": "app/views/admin/trends/links/index.html.haml",
|
||||
"rendered": {
|
||||
"name": "admin/trends/links/_preview_card",
|
||||
|
@ -213,13 +223,13 @@
|
|||
{
|
||||
"warning_type": "Mass Assignment",
|
||||
"warning_code": 105,
|
||||
"fingerprint": "e867661b2c9812bc8b75a5df12b28e2a53ab97015de0638b4e732fe442561b28",
|
||||
"fingerprint": "f9de0ca4b04ae4b51b74d98db14dcbb6dae6809e627b58e711019cf9b4a47866",
|
||||
"check_name": "PermitAttributes",
|
||||
"message": "Potentially dangerous key allowed for mass assignment",
|
||||
"file": "app/controllers/api/v1/reports_controller.rb",
|
||||
"line": 36,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
|
||||
"code": "params.permit(:account_id, :comment, :forward, :status_ids => ([]))",
|
||||
"code": "params.permit(:account_id, :comment, :category, :forward, :status_ids => ([]), :rule_ids => ([]))",
|
||||
"render_path": null,
|
||||
"location": {
|
||||
"type": "method",
|
||||
|
@ -231,6 +241,6 @@
|
|||
"note": ""
|
||||
}
|
||||
],
|
||||
"updated": "2021-11-14 05:26:09 +0100",
|
||||
"brakeman_version": "5.1.2"
|
||||
"updated": "2022-02-13 02:24:12 +0100",
|
||||
"brakeman_version": "5.2.1"
|
||||
}
|
||||
|
|
|
@ -94,7 +94,6 @@ en:
|
|||
account_moderation_notes:
|
||||
create: Leave note
|
||||
created_msg: Moderation note successfully created!
|
||||
delete: Delete
|
||||
destroyed_msg: Moderation note successfully destroyed!
|
||||
accounts:
|
||||
add_email_domain_block: Block e-mail domain
|
||||
|
@ -163,6 +162,11 @@ en:
|
|||
not_subscribed: Not subscribed
|
||||
pending: Pending review
|
||||
perform_full_suspension: Suspend
|
||||
previous_strikes: Previous strikes
|
||||
previous_strikes_description_html:
|
||||
one: This account has <strong>one</strong> strike.
|
||||
other: This account has <strong>%{count}</strong> strikes.
|
||||
zero: This account is <strong>in good standing</strong>.
|
||||
promote: Promote
|
||||
protocol: Protocol
|
||||
public: Public
|
||||
|
@ -227,6 +231,7 @@ en:
|
|||
whitelisted: Allowed for federation
|
||||
action_logs:
|
||||
action_types:
|
||||
approve_appeal: Approve Appeal
|
||||
approve_user: Approve User
|
||||
assigned_to_self_report: Assign Report
|
||||
change_email_user: Change E-mail for User
|
||||
|
@ -258,6 +263,7 @@ en:
|
|||
enable_user: Enable User
|
||||
memorialize_account: Memorialize Account
|
||||
promote_user: Promote User
|
||||
reject_appeal: Reject Appeal
|
||||
reject_user: Reject User
|
||||
remove_avatar_user: Remove Avatar
|
||||
reopen_report: Reopen Report
|
||||
|
@ -276,6 +282,7 @@ en:
|
|||
update_domain_block: Update Domain Block
|
||||
update_status: Update Post
|
||||
actions:
|
||||
approve_appeal_html: "%{name} approved moderation decision appeal from %{target}"
|
||||
approve_user_html: "%{name} approved sign-up from %{target}"
|
||||
assigned_to_self_report_html: "%{name} assigned report %{target} to themselves"
|
||||
change_email_user_html: "%{name} changed the e-mail address of user %{target}"
|
||||
|
@ -307,6 +314,7 @@ en:
|
|||
enable_user_html: "%{name} enabled login for user %{target}"
|
||||
memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page"
|
||||
promote_user_html: "%{name} promoted user %{target}"
|
||||
reject_appeal_html: "%{name} rejected moderation decision appeal from %{target}"
|
||||
reject_user_html: "%{name} rejected sign-up from %{target}"
|
||||
remove_avatar_user_html: "%{name} removed %{target}'s avatar"
|
||||
reopen_report_html: "%{name} reopened report %{target}"
|
||||
|
@ -385,6 +393,9 @@ en:
|
|||
media_storage: Media storage
|
||||
new_users: new users
|
||||
opened_reports: reports opened
|
||||
pending_appeals_html:
|
||||
one: "<strong>1</strong> pending appeal"
|
||||
other: "<strong>%{count}</strong> pending appeals"
|
||||
pending_reports_html:
|
||||
one: "<strong>1</strong> pending report"
|
||||
other: "<strong>%{count}</strong> pending reports"
|
||||
|
@ -402,6 +413,10 @@ en:
|
|||
top_languages: Top active languages
|
||||
top_servers: Top active servers
|
||||
website: Website
|
||||
disputes:
|
||||
appeals:
|
||||
empty: No appeals found.
|
||||
title: Appeals
|
||||
domain_allows:
|
||||
add_new: Allow federation with domain
|
||||
created_msg: Domain has been successfully allowed for federation
|
||||
|
@ -720,6 +735,16 @@ en:
|
|||
no_status_selected: No posts were changed as none were selected
|
||||
title: Account posts
|
||||
with_media: With media
|
||||
strikes:
|
||||
actions:
|
||||
delete_statuses: "%{name} deleted %{target}'s posts"
|
||||
disable: "%{name} froze %{target}'s account"
|
||||
none: "%{name} sent a warning to %{target}"
|
||||
sensitive: "%{name} marked %{target}'s account as sensitive"
|
||||
silence: "%{name} limited %{target}'s account"
|
||||
suspend: "%{name} suspended %{target}'s account"
|
||||
appeal_approved: Appealed
|
||||
appeal_pending: Appeal pending
|
||||
system_checks:
|
||||
database_schema_check:
|
||||
message_html: There are pending database migrations. Please run them to ensure the application behaves as expected
|
||||
|
@ -781,6 +806,17 @@ en:
|
|||
empty: You haven't defined any warning presets yet.
|
||||
title: Manage warning presets
|
||||
admin_mailer:
|
||||
new_appeal:
|
||||
actions:
|
||||
delete_statuses: to delete their posts
|
||||
disable: to freeze their account
|
||||
none: a warning
|
||||
sensitive: to mark their account as sensitive
|
||||
silence: to limit their account
|
||||
suspend: to suspend their account
|
||||
body: "%{target} is appealing a moderation decision by %{action_taken_by} from %{date}, which was %{type}. They wrote:"
|
||||
next_steps: You can approve the appeal to undo the moderation decision, or ignore it.
|
||||
subject: "%{username} is appealing a moderation decision on %{instance}"
|
||||
new_pending_account:
|
||||
body: The details of the new account are below. You can approve or reject this application.
|
||||
subject: New account up for review on %{instance} (%{username})
|
||||
|
@ -871,7 +907,6 @@ en:
|
|||
status:
|
||||
account_status: Account status
|
||||
confirming: Waiting for e-mail confirmation to be completed.
|
||||
functional: Your account is fully operational.
|
||||
pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved.
|
||||
redirecting_to: Your account is inactive because it is currently redirecting to %{acct}.
|
||||
too_fast: Form submitted too fast, try again.
|
||||
|
@ -937,6 +972,32 @@ en:
|
|||
directory: Profile directory
|
||||
explanation: Discover users based on their interests
|
||||
explore_mastodon: Explore %{title}
|
||||
disputes:
|
||||
strikes:
|
||||
action_taken: Action taken
|
||||
appeal: Appeal
|
||||
appeal_approved: This strike has been successfully appealed and is no longer valid
|
||||
appeal_rejected: The appeal has been rejected
|
||||
appeal_submitted_at: Appeal submitted
|
||||
appealed_msg: Your appeal has been submitted. If it is approved, you will be notified.
|
||||
appeals:
|
||||
submit: Submit appeal
|
||||
associated_report: Associated report
|
||||
created_at: Dated
|
||||
recipient: Addressed to
|
||||
status: 'Post #%{id}'
|
||||
status_removed: Post already removed from system
|
||||
title: "%{action} from %{date}"
|
||||
title_actions:
|
||||
delete_statuses: Post removal
|
||||
disable: Freezing of account
|
||||
none: Warning
|
||||
sensitive: Marking as sensitive of account
|
||||
silence: Limitation of account
|
||||
suspend: Suspension of account
|
||||
your_appeal_approved: Your appeal has been approved
|
||||
your_appeal_pending: You have submitted an appeal
|
||||
your_appeal_rejected: Your appeal has been rejected
|
||||
domain_validator:
|
||||
invalid_domain: is not a valid domain name
|
||||
errors:
|
||||
|
@ -1501,6 +1562,15 @@ en:
|
|||
recovery_instructions_html: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. <strong>Keep the recovery codes safe</strong>. For example, you may print them and store them with other important documents.
|
||||
webauthn: Security keys
|
||||
user_mailer:
|
||||
appeal_approved:
|
||||
action: Go to your account
|
||||
explanation: The appeal of the strike against your account on %{strike_date} that you submitted on %{appeal_date} has been approved. Your account is once again in good standing.
|
||||
subject: Your appeal from %{date} has been approved
|
||||
title: Appeal approved
|
||||
appeal_rejected:
|
||||
explanation: The appeal of the strike against your account on %{strike_date} that you submitted on %{appeal_date} has been rejected.
|
||||
subject: Your appeal from %{date} has been rejected
|
||||
title: Appeal rejected
|
||||
backup_ready:
|
||||
explanation: You requested a full backup of your Mastodon account. It's now ready for download!
|
||||
subject: Your archive is ready for download
|
||||
|
@ -1512,6 +1582,8 @@ en:
|
|||
subject: Please confirm attempted sign in
|
||||
title: Sign in attempt
|
||||
warning:
|
||||
appeal: Submit an appeal
|
||||
appeal_description: If you believe this is an error, you can submit an appeal to the staff of %{instance}.
|
||||
categories:
|
||||
spam: Spam
|
||||
violation: Content violates the following community guidelines
|
||||
|
@ -1523,7 +1595,6 @@ en:
|
|||
suspend: You can no longer use your account, and your profile and other data are no longer accessible. You can still login to request a backup of your data until the data is fully removed in about 30 days, but we will retain some basic data to prevent you from evading the suspension.
|
||||
get_in_touch: If you believe this is an error, you can reply to this e-mail to get in touch with the staff of %{instance}.
|
||||
reason: 'Reason:'
|
||||
review_server_policies: Review server policies
|
||||
statuses: 'Posts that have been found in violation:'
|
||||
subject:
|
||||
delete_statuses: Your posts on %{acct} have been removed
|
||||
|
|
|
@ -27,6 +27,8 @@ en:
|
|||
scheduled_at: Leave blank to publish the announcement immediately
|
||||
starts_at: Optional. In case your announcement is bound to a specific time range
|
||||
text: You can use post syntax. Please be mindful of the space the announcement will take up on the user's screen
|
||||
appeal:
|
||||
text: You can only appeal a strike once
|
||||
defaults:
|
||||
autofollow: People who sign up through the invite will automatically follow you
|
||||
avatar: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
|
||||
|
@ -119,6 +121,8 @@ en:
|
|||
scheduled_at: Schedule publication
|
||||
starts_at: Start of event
|
||||
text: Announcement
|
||||
appeal:
|
||||
text: Explain why this decision should be reversed
|
||||
defaults:
|
||||
autofollow: Invite to follow your account
|
||||
avatar: Avatar
|
||||
|
@ -197,6 +201,7 @@ en:
|
|||
sign_up_requires_approval: Limit sign-ups
|
||||
severity: Rule
|
||||
notification_emails:
|
||||
appeal: Someone appeals a moderator decision
|
||||
digest: Send digest e-mails
|
||||
favourite: Someone favourited your post
|
||||
follow: Someone followed you
|
||||
|
@ -204,8 +209,8 @@ en:
|
|||
mention: Someone mentioned you
|
||||
pending_account: New account needs review
|
||||
reblog: Someone boosted your post
|
||||
report: A new report is submitted
|
||||
trending_tag: A new trend requires approval
|
||||
report: New report is submitted
|
||||
trending_tag: New trend requires review
|
||||
rule:
|
||||
text: Rule
|
||||
tag:
|
||||
|
|
|
@ -20,7 +20,7 @@ SimpleNavigation::Configuration.run do |navigation|
|
|||
n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_url, if: -> { current_user.functional? }
|
||||
|
||||
n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_url do |s|
|
||||
s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities}
|
||||
s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities|^/disputes}
|
||||
s.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_methods_url, highlights_on: %r{/settings/two_factor_authentication|/settings/otp_authentication|/settings/security_keys}
|
||||
s.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
|
||||
end
|
||||
|
@ -41,7 +41,7 @@ SimpleNavigation::Configuration.run do |navigation|
|
|||
n.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), admin_reports_url, if: proc { current_user.staff? } do |s|
|
||||
s.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_url
|
||||
s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports}
|
||||
s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts}
|
||||
s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts|/admin/disputes}
|
||||
s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path
|
||||
s.item :follow_recommendations, safe_join([fa_icon('user-plus fw'), t('admin.follow_recommendations.title')]), admin_follow_recommendations_path, highlights_on: %r{/admin/follow_recommendations}
|
||||
s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.admin? }
|
||||
|
|
|
@ -164,6 +164,12 @@ Rails.application.routes.draw do
|
|||
resources :login_activities, only: [:index]
|
||||
end
|
||||
|
||||
namespace :disputes do
|
||||
resources :strikes, only: [:show] do
|
||||
resource :appeal, only: [:create]
|
||||
end
|
||||
end
|
||||
|
||||
resources :media, only: [:show] do
|
||||
get :player
|
||||
end
|
||||
|
@ -324,6 +330,15 @@ Rails.application.routes.draw do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
namespace :disputes do
|
||||
resources :appeals, only: [:index] do
|
||||
member do
|
||||
post :approve
|
||||
post :reject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
get '/admin', to: redirect('/admin/dashboard', status: 302)
|
||||
|
|
|
@ -48,6 +48,7 @@ defaults: &defaults
|
|||
report: true
|
||||
pending_account: true
|
||||
trending_tag: true
|
||||
appeal: true
|
||||
interactions:
|
||||
must_be_follower: false
|
||||
must_be_following: false
|
||||
|
|
14
db/migrate/20220124141035_create_appeals.rb
Normal file
14
db/migrate/20220124141035_create_appeals.rb
Normal file
|
@ -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
|
23
db/schema.rb
23
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2022_01_18_183123) do
|
||||
ActiveRecord::Schema.define(version: 2022_02_10_153119) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -135,6 +135,7 @@ ActiveRecord::Schema.define(version: 2022_01_18_183123) do
|
|||
t.datetime "updated_at", null: false
|
||||
t.bigint "report_id"
|
||||
t.string "status_ids", array: true
|
||||
t.datetime "overruled_at"
|
||||
t.index ["account_id"], name: "index_account_warnings_on_account_id"
|
||||
t.index ["target_account_id"], name: "index_account_warnings_on_target_account_id"
|
||||
end
|
||||
|
@ -243,6 +244,22 @@ ActiveRecord::Schema.define(version: 2022_01_18_183123) do
|
|||
t.bigint "status_ids", array: true
|
||||
end
|
||||
|
||||
create_table "appeals", force: :cascade do |t|
|
||||
t.bigint "account_id", null: false
|
||||
t.bigint "account_warning_id", null: false
|
||||
t.text "text", default: "", null: false
|
||||
t.datetime "approved_at"
|
||||
t.bigint "approved_by_account_id"
|
||||
t.datetime "rejected_at"
|
||||
t.bigint "rejected_by_account_id"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.index ["account_id"], name: "index_appeals_on_account_id"
|
||||
t.index ["account_warning_id"], name: "index_appeals_on_account_warning_id", unique: true
|
||||
t.index ["approved_by_account_id"], name: "index_appeals_on_approved_by_account_id"
|
||||
t.index ["rejected_by_account_id"], name: "index_appeals_on_rejected_by_account_id"
|
||||
end
|
||||
|
||||
create_table "backups", force: :cascade do |t|
|
||||
t.bigint "user_id"
|
||||
t.string "dump_file_name"
|
||||
|
@ -1031,6 +1048,10 @@ ActiveRecord::Schema.define(version: 2022_01_18_183123) do
|
|||
add_foreign_key "announcement_reactions", "accounts", on_delete: :cascade
|
||||
add_foreign_key "announcement_reactions", "announcements", on_delete: :cascade
|
||||
add_foreign_key "announcement_reactions", "custom_emojis", on_delete: :cascade
|
||||
add_foreign_key "appeals", "account_warnings", on_delete: :cascade
|
||||
add_foreign_key "appeals", "accounts", column: "approved_by_account_id", on_delete: :nullify
|
||||
add_foreign_key "appeals", "accounts", column: "rejected_by_account_id", on_delete: :nullify
|
||||
add_foreign_key "appeals", "accounts", on_delete: :cascade
|
||||
add_foreign_key "backups", "users", on_delete: :nullify
|
||||
add_foreign_key "blocks", "accounts", column: "target_account_id", name: "fk_9571bfabc1", on_delete: :cascade
|
||||
add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade
|
||||
|
|
53
spec/controllers/admin/disputes/appeals_controller_spec.rb
Normal file
53
spec/controllers/admin/disputes/appeals_controller_spec.rb
Normal file
|
@ -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
|
27
spec/controllers/disputes/appeals_controller_spec.rb
Normal file
27
spec/controllers/disputes/appeals_controller_spec.rb
Normal file
|
@ -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
|
30
spec/controllers/disputes/strikes_controller_spec.rb
Normal file
30
spec/controllers/disputes/strikes_controller_spec.rb
Normal file
|
@ -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
|
||||
account nil
|
||||
target_account nil
|
||||
text "MyText"
|
||||
account
|
||||
target_account(fabricator: :account)
|
||||
text { Faker::Lorem.paragraph }
|
||||
action 'suspend'
|
||||
end
|
||||
|
|
5
spec/fabricators/appeal_fabricator.rb
Normal file
5
spec/fabricators/appeal_fabricator.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
Fabricator(:appeal) do
|
||||
strike(fabricator: :account_warning)
|
||||
account { |attrs| attrs[:strike].target_account }
|
||||
text { Faker::Lorem.paragraph }
|
||||
end
|
|
@ -15,4 +15,9 @@ class AdminMailerPreview < ActionMailer::Preview
|
|||
def new_trending_links
|
||||
AdminMailer.new_trending_links(Account.first, PreviewCard.limit(3))
|
||||
end
|
||||
|
||||
# Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_appeal
|
||||
def new_appeal
|
||||
AdminMailer.new_appeal(Account.first, Appeal.first)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -82,6 +82,11 @@ class UserMailerPreview < ActionMailer::Preview
|
|||
UserMailer.warning(User.first, AccountWarning.last)
|
||||
end
|
||||
|
||||
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/appeal_approved
|
||||
def appeal_approved
|
||||
UserMailer.appeal_approved(User.first, Appeal.last)
|
||||
end
|
||||
|
||||
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/sign_in_token
|
||||
def sign_in_token
|
||||
UserMailer.sign_in_token(User.first.tap { |user| user.generate_sign_in_token }, '127.0.0.1', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0', Time.now.utc)
|
||||
|
|
5
spec/models/appeal_spec.rb
Normal file
5
spec/models/appeal_spec.rb
Normal file
|
@ -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 a new issue