forked from berserker/microblog
Conflicts: - `db/schema.rb`: Conflict due to glitch-soc adding the `content_type` column on status edits and thus having a different schema version number. Solved by taking upstream's schema version number, as it is higher than glitch-soc's.local
commit
f224237862
81 changed files with 1461 additions and 381 deletions
@ -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,26 @@ |
||||
# 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 => e |
||||
@appeal = e.record |
||||
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,60 @@ |
||||
# 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 |
||||
MAX_STRIKE_AGE = 20.days |
||||
|
||||
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 strike.created_at < MAX_STRIKE_AGE.ago |
||||
end |
||||
end |
@ -0,0 +1,17 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class AccountWarningPolicy < ApplicationPolicy |
||||
def show? |
||||
target? || staff? |
||||
end |
||||
|
||||
def appeal? |
||||
target? && record.created_at >= Appeal::MAX_STRIKE_AGE.ago |
||||
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 |
||||
.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') |
||||
|
@ -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') |
||||
|
||||
.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/ |
||||
|
@ -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 |
||||
account nil |
||||
target_account nil |
||||
text "MyText" |
||||
account |
||||
target_account(fabricator: :account) |
||||
text { Faker::Lorem.paragraph } |
||||
action 'suspend' |
||||
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